Optimistic Concurrency

ScaleOut Software NamedCache API

Previous versions of ScaleOut StateServer allowed clients to acquire an exclusive (pessimistic) lock on an object across the entire distributed cache, enabling clients to make safe, consistent updates to the cache. Version 5 introduces the ability to perform optimistic updates as an alternative approach to concurrency control.

Optimistic locking is a practical option when the chances of an update collision are low (for example, in applications that rarely perform updates in the distributed cache, or in applications that rarely update the same objects at the same time). Instead of preventing other clients from accessing the object, the optimistic strategy raises an exception only if a collision is detected, allowing the client application to resolve the collision (typically by retrying the operation). If the odds of an update collision are too high then optimistic updates should be avoided, since resolving the collision could become expensive if done frequently.

ScaleOut StateServer supports optimistic locking by performing a version check when an object is updated in the distributed cache. If the version in the StateServer is newer than the instance being used in the update then an OptimisticLockException will be thrown, indicating that another client performed an update in the interval between the object's retrieval and the update.

IOptimisticConcurrencyVersionHolder

The version information used for optimistic updates is stored in the object itself--in order to support optimistic operations, types must implement the IOptimisticConcurrencyVersionHolder interface and its single property (which requires both a getter and a setter). The SOSS NamedCache API will automatically manage the version information for classes that implement this interface.

Implementing IOptimisticConcurrencyVersionHolder
/// <summary>
/// Class containing user information. Per-user objects like this are good candidates
/// for optimistic locking because it's unlikely that two different clients or 
/// threads will update an instance at the same time.
/// </summary>
[Serializable]
class UserDetail : IOptimisticConcurrencyVersionHolder
{
    public String FullName { get; set; }
    public String ZipCode { get; set; }

    // IOptimisticConcurrencyVersionHolder Member:
    public OptimisticConcurrencyVersion OptimisticConcurrencyVersion { get; set; }
}

Performing an Optimistic Update

An optimistic update can be performed by using the Update method. New update overloads have been added that take an UpdateOptions struct as a parameter, whose LockingMode property allows you to specify a UpdateLockingMode. This enum controls whether the update operation performs optimistic locking, pessimistic locking, or no locking at all.

Optimistic Update
// Create the object and add it to the cache:
UserDetail userDetail = new UserDetail
{
    FullName = "John Public",
    ZipCode = "98004"
};
NamedCache nc = CacheFactory.GetCache("User Details");
nc.Insert("User1", userDetail, nc.DefaultCreatePolicy, false, false);

// Change the object and do an optimisitic update in the cache:
userDetail.FullName = "John Q. Public";
UpdateOptions updateOptions = new UpdateOptions(UpdateLockingMode.UpdateIfSameVersion);
nc.Update("User1", userDetail, updateOptions);

Resolving Collisions

In the event of an update collision, most applications will want to re-retrieve the object and attempt the optimistic update again.

Retrying an Update
/// <summary>
/// Retrieve an object from the cache and attempt an optimistic update.
/// </summary>
public static void PerformOptimisticUpdate()
{
    // We attempt the optimistic update in a while loop because we'll 
    // want to re-retrieve and try again if we get an OptimisticLockException.
    int retryCount = 0;
    const int maxRetryCount = 100;

    while (true)
    {
        if (retryCount == maxRetryCount)
        {
            throw new ApplicationException("Unable to perform optimistic update.");
        }

        NamedCache nc = CacheFactory.GetCache("User Details");

        // Retrieve the cached object and change its state.
        UserDetail userDetail = nc.Retrieve("User1", false) as UserDetail;
        if (userDetail != null)
        {
            userDetail.FullName = "John Q. Public";
            try
            {
                UpdateOptions updateOptions = new UpdateOptions(UpdateLockingMode.UpdateIfSameVersion);
                nc.Update("User1", userDetail, updateOptions);
                break;
            }
            catch (OptimisticLockException)
            {
                retryCount++;
                continue;
            }
        }
        else
        {
            // Another client or thread must have removed our object.
            // Recreate it with our desired value.
            CacheUserDetails("John Q. Public", "98004");
            break;
        }

    }
}

Optimistic Locking and the Client Cache

The NamedCache may return the same instance of an object to two different threads that retrieve the object from the distributed cache. This can occur because the SOSS client libraries maintain a near cache of recently-accessed objects in order to cut down on network and serialization overhead.

This client-side caching can have repercussions for applications that use optimistic locking--one thread's changes to an object that was retrieved from the client cache will be visible to all other threads referencing that same client cache instance, even if the changes have not yet been pushed to the authoritative SOSS server. This can cause trouble if an optimistic update fails because the copy of the object in the client cache will be out of sync with the authoritative version in the server--all threads in the client would continue to see the changed object, even after the update failure.

One of three approaches should be taken to avoid the risk of putting the client cache in a state that is inconsistent with the server:

  • If an OptimisticLockException is thrown from the Update operation, immediately re-retrieve the object from the SOSS server so as to re-synchronize the client cache with the latest version of the object.
  • Make changes to a deep copy of the object before changing its state and attempting an optimistic update.
  • Use the NamedCache.AllowClientCaching property to disable the client cache for the NamedCache in question.