Optimistic Locking with Couchbase

Today I’d like to spend some time covering how to handle optimistic locking using the .NET Couchbase client. Couchbase covers optimistic locking pretty well when talking about the CAS (check and set) method options they have within their documentation. I tried doing some research on Google to see if I could find any good examples of an actual implementation via the .NET client but was unable to find anything so naturally I created my own way and hopefully by writing this blog post I will be able to help someone else out in the future.

I’ll focus on the update operation here as that is mainly what I care about when it comes to optimistic locking. The important thing to remember is that when you create your Couchbase model you will want to ensure you have a property that can hold the CAS value that Couchbase will create when you first store a document or update a document. In my case I have a base model class that has 3 properties; Key, CasValue, and Type. Then each model inherits the model base so that these properties are available on every model. When I create a document or get a document I set the CasValue and Key value so that my logic can use those values later on. The Key property is the document key that can be used to retrieve the document in the future from Couchbase. The Type property is an abstract property and is overridden by each model with the appropriate model type. This property will make it easy to narrow our focus down when creating views within Couchbase. The CasValue property is the CAS value that is generated by Couchbase and is the key to optimistic locking. When I want to update an existing document in Couchbase I need to be sure that the CasValue matches the CasValue currently in Couchbase for the document I’m trying to update. If the value in Couchbase is different, that means another process has updated the document so we need to get the latest document and apply the changes to the document and then re-save the document. Otherwise the existing updates done by the other process will be overwritten. To accomplish this task I used an anonymous delegate also known as a lambda expression. Below is the code created for handling the update and retrying if we find that the document has been altered since we last retrieved it from Couchbase. I’ve left out some details like how the CreateDocument method works so that you can try to add that logic yourself.

/// Load a clean copy of the model and passes it to the given
/// block, which should apply changes to that model that can
/// be retried from scratch multiple times until successful.
///
/// This method will return the final state of the saved model.
/// The caller should use this afterward, instead of the instance
/// it had passed in to the call.
///
///The model.
///The block.
/// the latest version of the model provided to the method
public virtual T UpdateDocumentWithRetry(T model, Action block)
{
    var documentKey = model.Key;
    var success = false;
    T documentToReturn = null;

    while (!success)
    {
        // load a clean copy of the document
        var latestDocument = GetDocument(documentKey);
        // if we were unable to find a document then create the document and
        // return the latest version of the document to the caller
        if (latestDocument == null)
        {
            // document doesn't exist so Add it here and then exit the
            // method returning the document that was saved to Couchbase
            return CreateDocument(model);
        }

        // pass the latest document to the given block so the latest changes can be applied
        if (block != null)
            block(latestDocument);

        var latestDocumentJson = latestDocument.ToJson();
        var storeResult = Client.ExecuteCas(StoreMode.Replace, latestDocument.Key, latestDocumentJson,
                                            latestDocument.CasValue);
        if (!storeResult.Success)
            continue;

        documentToReturn = latestDocument;
        success = true;
    }

    return documentToReturn;
}

Here is a test example of how to use the UpdateDocumentWithRetry code from above. Keep in mind that the below test is just using fictitious information to illustrate how to use the UpdateDocumentWithRetry method and will need to be updated to your data model logic.

[Test]
public void TestUpdateAndRetry()
{
    var documentKey = "document::1";
    var originalDocument = new Document
        {
            update_count = 1,
            last_update_time = DateTime.UtcNow
        };

    originalDocument = Client.Store(StoreMode.Add, documentKey, originalDocument);

    var doc1 = Client.Get(documentKey);
    var doc2 = Client.Get(documentKey);

    doc1.update_count = originalDocument.update_count + 1;
    doc1 = Client.Store(StoreMode.Set, documentKey, doc1);

    var count = 0;
    doc2 = UpdateDocumentWithRetry(doc2, doc =>
            {
                count += 1;
                if (count == 1)
                {
                    doc1.update_count = doc1.update_count + 1;
                    Client.Store(StoreMode.Set, documentKey, doc1);
                }

                doc.update_count = doc.update_count + 1;
            });

    Assert.AreEqual(count, 2);

    var latestDocument = Client.Get(documentKey);
    Assert.AreEqual(doc1.update_count, 3);
    Assert.AreEqual(doc2.update_count, 4);
    Assert.AreEqual(latestDocument.update_count, 4);
}

Hopefully this will help you get started with implementing optimistic locking in your application with whatever NoSQL solution you are using. You can also read more on CAS operations in the Couchbase .NET 1.2 client by going here: Couchbase CAS Method Documentation

UPDATED 11/11/2013: My colleague Scott W. Bradley has just posted a similar article with sample Ruby code.

Tagged with: , ,
Posted in C#, Couchbase

Leave a Reply