Handle concurrency using session.Lock

It's often important to handle scenarios where two or more clients may try to work with the same entities, concurrently. In Chapter 2, Models and Mappings, in section: Handling versioning and concurrency we discussed how to handle that using versioning and optimistic concurrency. A more aggressive approach is to use pessimistic concurrency, where specific rows in the database are explicitly locked for certain operations. Many DBMSes, such as SQL Server, has a good support for this and in this recipe we'll show how NHibernate can use that functionality.

Getting ready

Follow the Getting ready step in the Save entities to the database recipe in this chapter.

How to do it…

Since this recipe requires specific database support, you can't run it using the SQLite database, which is the default in NH4CookbookHelpers. Reconfigure the recipe runner by setting the RecipeLoader.DefaultConfiguration property or simply override the Configure method in the Recipe class to provide a database configuration specific to this recipe.

  1. Create a new folder named SessionLock in the project.
  2. Add a new class named Recipe to the folder:
    using System;
    using System.Threading;
    using NH4CookbookHelpers.Queries;
    using NH4CookbookHelpers.Queries.Model;
    using NHibernate;
    
    namespace SessionRecipes.SessionLock
    {
      public class Recipe : QueryRecipe
      {
        protected override void Run(ISessionFactory 
          sessionFactory)
        {
          ExecuteWithLockMode(sessionFactory, 
            LockMode.None,
            LockMode.None);
    
          ExecuteWithLockMode(sessionFactory, 
            LockMode.Upgrade,
            LockMode.Upgrade);
    
          ExecuteWithLockMode(sessionFactory, 
            LockMode.UpgradeNoWait, 
            LockMode.UpgradeNoWait);
        }
    
        private void ExecuteWithLockMode(ISessionFactory 
          sessionFactory, LockMode lockMode1, 
          LockMode lockMode2)
        {
          Console.WriteLine("Executing with {0} and {1}", 
            lockMode1, lockMode2);
          Console.WriteLine();
    
          var thread1 = new Thread(() =>
            GetAndChangeProductInLock(sessionFactory, 
              lockMode1, 3000)) { 
              Name = "Thread1"
          };
    
          var thread2 = new Thread(() =>
            GetAndChangeProductInLock(sessionFactory, 
              lockMode2, 0)) { 
              Name = "Thread2"
          };
    
          thread1.Start();
    
          Thread.Sleep(300);
    
          thread2.Start();
    
          thread1.Join();
          thread2.Join();
          Console.WriteLine();
        }
    
        private void GetAndChangeProductInLock(ISessionFactory
          sessionFactory, LockMode lockMode, int sleepTime)
        {
          try
          {
            using (var session = sessionFactory.OpenSession())
            {
              using (var tx = session.BeginTransaction())
              {
                var product = session.Get<Product>(1);
                Console.WriteLine("{0} acquiring lock", 
                Thread.CurrentThread.Name);
    
                session.Lock(product, lockMode);
    
                Console.WriteLine("{0} acquired lock", 
                Thread.CurrentThread.Name);
    
                product.Description = 
                  string.Format("Updated in LockMode.{0}",
                    lockMode);
    
                Thread.Sleep(sleepTime);
    
                Console.WriteLine("{0} committing", 
                  Thread.CurrentThread.Name);
    
                tx.Commit();
    
                Console.WriteLine("{0} committed", 
                  Thread.CurrentThread.Name);
              }
            }
          }
          catch (Exception ex)
          {
            Console.WriteLine("Exception in {0}:{1}",
              Thread.CurrentThread.Name, ex.Message);
          }
        }
      }
    }
  3. Run the application and start the SessionLock recipe. You should be able to see this output:
    How to do it…

How it works…

Session.Lock uses database specific methods to acquire locks on specific rows, in order to prevent concurrent access to the same entities from multiple clients. In our recipe, we try to simulate this scenario, using two separate threads executing almost simultaneously. The first thread will sleep for three seconds before it commits its transaction, and the second thread will have to act accordingly. How it acts depends on the LockMode used.

None

This is the default lock mode. No specific lock is required. However, if the database becomes involved (the object was not found in any cache), a read lock may be acquired.

Read

This is a shared lock, which will be acquired implicitly if the transaction's serialization level is RepeatableRead or serializable.

Upgrade

When this lock mode is used, NHibernate will issue a SELECT WITH (updlock,rowlock) query. This query will not return anything until any previous locks have been released. This can be seen in the recipe output, where the second thread doesn't acquire its lock until the first thread has committed its transaction:

  • Thread1 acquiring lock
  • Thread1 acquired lock
  • Thread2 acquiring lock
  • Thread1 committed
  • Thread2 acquired lock
  • Thread2 committed

If the transaction owning the lock takes a lot of time to complete, a timeout exception may be thrown in the transaction which tries to acquire the new lock.

UpgradeNoWait

This lock mode, which requires specific database support, behaves similarly to Upgrade. However, instead of waiting for a previous lock to be released, it will fail immediately if the lock couldn't be acquired.

  • Thread1 acquiring lock
  • Thread1 acquired lock
  • Thread2 acquiring lock
Exception in Thread2:could not lock:[NH4CookbookHelpers.Queries.Model.Movie#1][SQL: SELECT Id FROM Product with (updlock, rowlock, nowait) WHERE Id = ?]
Thread1 committed
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.87.161