I'm trying to create a unique counter table. To update the counter in the table I tried using FindOneAndUpdate. At first everything seem to work as planned so I figured I would stress test it. I created a process that gets a counter 20,000 and executed that counter on 5 threads. Sure enough, I end up with duplicates. I'm running this against a single, local db; however the production db is a replica set.
static void Main(string[] args)
{
Console.WriteLine("Press Enter to begin...");
Console.ReadLine();
int numOfThreads = 10;
var tasks = new List<Task>();
for (int i = 0; i < numOfThreads; i++)
tasks.Add(Task.Run(() => TestWrites()));
Console.WriteLine($"Waiting for {tasks.Count} tasks to complete...");
Task.WaitAll(tasks.ToArray());
Console.WriteLine("Done. Press Enter to exit.");
Console.ReadLine();
}
private static void TestWrites()
{
var tester = new ConcurrentTester();
int writes = 20000;
for (int i = 0; i < writes; i++)
tester.InsertCounter(tester.GetCounter());
Console.WriteLine($"Wrote {writes}");
}
public class ConcurrentTester
{
private IMongoDatabase database;
public ConcurrentTester()
{
var connStr = "";
var mongoClient = new MongoClient(connStr);
database = mongoClient.GetDatabase("test");
}
public int GetCounter()
{
var collection = database.GetCollection<InternalCounter>("Concurrency");
var filter = Builders<InternalCounter>.Filter.Eq("CounterName", "Test");
var mkt = collection.Find(filter).FirstOrDefault();
var options = new FindOneAndUpdateOptions<InternalCounter> { IsUpsert = true, ReturnDocument = ReturnDocument.After };
InternalCounter ctn = collection.FindOneAndUpdate(filter,
Builders<InternalCounter>.Update.Inc(s => s.CounterValue, 1), options);
return ctn.CounterValue;
}
}
public class InternalCounter
{
public InternalCounter()
{
_id = ObjectId.GenerateNewId();
}
public object _id { get; set; }
public string CounterName { get; set; }
public int CounterValue { get; set; }
}
Update
Since this code is in our service, I added a lock to prevent concurrent calls. This is only a stop gap until I can figure out what's going on in MongoDb.
lock (sync)
{
ctn = collection.FindOneAndUpdate(filter,
Builders<InternalCounter>.Update.Inc(s => s.CounterValue, 1), options);
}
Update 2
I see there is also a findAndModify method but it's not implemented in the C# lib. From it's description it sounds like it supposed to do the same as findOneAndUpdate. I also read that findOneAndUpdate replaced findAndModify. Is this true?
Update 3 After further consideration, my lock isn't going to work. It will work when there is only a single instance but with load balancing, there are multiple instances and that can't be fixed with a lock.