2

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.

jbassking10
  • 833
  • 4
  • 15
  • 42

1 Answers1

1

If you don't have a unique index on "CounterName" field, you can end up with duplicate rows for the same counter. This is described in MongoDB docs.

Asya Kamsky
  • 41,784
  • 5
  • 109
  • 133