Dio F
Dio F

Reputation: 2538

NHibernate data is out of sync in a multi-threaded application

I use NHibernate in a multi-threaded scenario. It's a test that performs the following actions.

  1. (Start a multi-threaded server.)
  2. Send a request to the server. The server creates a new Foo object with state A and stores it in the DB using NHibernate.
  3. Set state B for the newly created Foo object.
  4. Send a request to the server. The server gets Foo from the DB using NHibernate, reads its state (should be B) and acts accordingly.
  5. (Stop the server.)

Now my problem is that sporadically (in 1 - 2% of the cases) in step 4 the server reads state A when it should read B and the test fails. What am I doing wrong here?

NHibernate sessions are retrieved via CurrentSessionContext which is set to thread static. I use Fluent NHibernate, but I guess this doesn't matter.

Edit

Details about the server: The server is a simple TCP server. When started it creates a new thread task to wait for incoming connections. When a new connection is accepted it creates another thread task to process the request from the connection and send a response. The the processing thread task terminates.

The server uses Task.Factory.StartNew() to create new threads tasks.

Edit 2

I wrote a small NUnit test to reproduce this behavior. It fails with

Expected: Open But was: Initialized

on the first iteration.

[Test]
[Explicit]
public void NHibernateProblem()
{
    for (var i = 0; i < 100; i++)
    {
        var identifier = Guid.NewGuid().ToString();
        var session1 = Database.GetCurrentSession();
        using (var dbTransaction = session1.BeginTransaction())
        {
            var transaction1 = new Transaction
            {
                Identifier = identifier,
                Status = TransactionStatus.Initialized
            };

            session1.SaveOrUpdate(transaction1);
            dbTransaction.Commit();
        }

        Task.Factory.StartNew(() =>
        {
            var session2 = Database.GetCurrentSession();
            using (var dbTransaction = session2.BeginTransaction())
            {
                var transaction2 = session2
                    .QueryOver<Transaction>()
                    .Where(t => t.Identifier == identifier)
                    .SingleOrDefault();

                transaction2.Status = TransactionStatus.Open;

                session2.SaveOrUpdate(transaction2);
                dbTransaction.Commit();
            }
        }).Wait();

        var session3 = Database.GetCurrentSession();
        using (var dbTransaction = session3.BeginTransaction())
        {
            var transaction3 = session3
                .QueryOver<Transaction>()
                .Where(t => t.Identifier == identifier)
                .SingleOrDefault();

            dbTransaction.Commit();
            Console.WriteLine("iteration {0}", i);

            Assert.That(transaction3.Status, Is.EqualTo(TransactionStatus.Open));
        }
    }
}

Database.GetCurrentSession() looks like this:

public virtual ISession GetCurrentSession()
{
    if (CurrentSessionContext.HasBind(SessionFactory))
    {
        var currentSession = SessionFactory.GetCurrentSession();
        return currentSession;
    }

    var session = SessionFactory.OpenSession();
    session.FlushMode = FlushMode.Always;
    session.CacheMode = CacheMode.Ignore;
    CurrentSessionContext.Bind(session);
    return session;
}

I believe the mappings are less relevant.

Upvotes: 0

Views: 877

Answers (2)

Low Flying Pelican
Low Flying Pelican

Reputation: 6054

In a server general practice is to create a session per request, and to start the transaction at the beginning of the request. When the request is about to complete transaction is either committed or rolled back depending on the result.

In your test it does not actually simulate this scenario, and it uses the same session for request 1 and request 3 where it will never be the case in actual scenario.

So, to test the actual scenario, you can create a new session for session 3.

Upvotes: 1

Oskar Berggren
Oskar Berggren

Reputation: 5629

In the unit test shown, session3 will be the same as session1, and therefore transaction3 will be the same object instance as transaction1, and it will therefore still show the values as they were when the transaction-object was added.

The query in the third part will hit the database, but NHibernate will see that the fetched object is already loaded and return the instance that the session already holds in memory. It will normally not overwrite the values in the object.

Upvotes: 1

Related Questions