OK, I've boiled it down to something that I can just paste here. Obviously, you'd need the Mongo driver dlls and NUnit to run this. The SingleThreadTest passes every time. The MultiThreadedTest fails every time, but sometimes with different results :)
using System.Linq;
using System.Threading;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
using MongoDB.Driver.Linq;
using NUnit.Framework;
namespace ThreadFail
{
public class CounterDocument
{
[BsonId]
public string KeyName { get; set; }
public int KeyValue { get; set; }
}
[TestFixture]
public class Tests
{
private MongoServer _server;
private MongoDatabase _database;
private const string testCollectionName = "CounterDocuments";
private const string testCounterName = "mycounter";
[SetUp]
public void SetUp()
{
// NUnit runs this before each test
_server = MongoServer.Create("mongodb://localhost:9123");
_database = _server.GetDatabase("threadfail", SafeMode.True);
_database.DropCollection(testCollectionName);
}
private int IncrementCounter(string keyName)
{
var collection = _database.GetCollection<CounterDocument>(testCollectionName);
var existing = collection.AsQueryable().SingleOrDefault(foo => foo.KeyName == keyName);
if (existing == null)
{
collection.Insert(new CounterDocument() { KeyName = keyName, KeyValue = 0 });
}
var val = AdjustCounterAndReturnAdjustedValue(collection, keyName, 1);
return val;
}
private int AdjustCounterAndReturnAdjustedValue(MongoCollection collection, string keyName, int adjustmentAmount)
{
var query = Query.And(Query.EQ("_id", keyName));
var sortBy = SortBy.Null;
var update = Update.Inc("KeyValue", adjustmentAmount);
var result = collection.FindAndModify(query, sortBy, update, true);
var val = result.GetModifiedDocumentAs<CounterDocument>().KeyValue;
return val;
}
[Test]
public void SingleThreadedTest()
{
IncrementTheCounter1000Times();
var valueNow = IncrementCounter(testCounterName);
Assert.That(valueNow == 1001, "should have been 1001 but was {0}", valueNow);
}
[Test]
public void MultiThreadedTest()
{
var thread1 = new Thread(IncrementTheCounter1000Times);
var thread2 = new Thread(IncrementTheCounter1000Times);
var thread3 = new Thread(IncrementTheCounter1000Times);
var thread4 = new Thread(IncrementTheCounter1000Times);
thread1.Start();
thread2.Start();
thread3.Start();
thread4.Start();
while (thread1.IsAlive || thread2.IsAlive || thread3.IsAlive || thread4.IsAlive)
{
// keep the unit test alive
}
var valueNow = IncrementCounter(testCounterName);
Assert.That(valueNow == 4001, "should be up to 4001 now, but was {0}", valueNow);
}
private void IncrementTheCounter1000Times()
{
for (int i = 0; i < 1000; i++)
{
IncrementCounter(testCounterName);
}
}
}
}