Compare Exchange Overview



Compare Exchange Transaction Scope

  • Since the compare-exchange operations guarantee atomicity across the entire cluster, the feature is not using the transaction associated with a session object, as a session transaction spans only a single node.

  • So if a compare-exchange operation has failed when used inside a session block, it will not be rolled back automatically upon a session transaction failure.

Creating a Key

  • Provide the following when saving a key:
Parameter Description
Key A string under which Value is saved, unique in the database scope across the cluster.
Value The Value that is associated with the Key.
Can be a number, string, boolean, array or any JSON formatted object.
Index The Index number is indicating the version of Value.
The Index is used for the concurrency control, similar to documents Etags.
  • When creating a new 'Compare Exchange Key', the index should be set to 0.

  • The Put operation will succeed only if this key doesn't exist yet.

  • Note: Two different keys can have the same values as long as the keys are unique.

Updating a Key

  • Updating a 'Compare Exchange' key can be divided into 2 phases:

    1. Get the existing Key. The associated Value and Index are received.

    2. The Index obtained from the read operation is provided to the Put operation along with the new Value to be saved.
      This save will succeed only if the index that is provided to the 'Put' operation is the same as the index that was received from the server in the previous 'Get', which means that the Value was not modified by someone else between the read and write operations.

Example I - Email Address Reservation

  • Compare Exchange can be used to maintain uniqueness across users emails accounts.

  • First try to reserve a new user email.
    If the email is successfully reserved then save the user account document.

string email = "user@example.com";

User user = new User
{
    Email = email
};

using (IDocumentSession session = store.OpenSession())
{
    session.Store(user);
    // At this point, the user document has an Id assigned

    // Try to reserve a new user email 
    // Note: This operation takes place outside of the session transaction, 
    //       It is a cluster-wide reservation
    CompareExchangeResult<string> cmpXchgResult
        = store.Operations.Send(
            new PutCompareExchangeValueOperation<string>("emails/" + email, user.Id, 0));

    if (cmpXchgResult.Successful == false)
        throw new Exception("Email is already in use");

    // At this point we managed to reserve/save the user email -
    // The document can be saved in SaveChanges
    session.SaveChanges();
}

Implications:

  • The User object is saved as a document, hence it can be indexed, queried, etc.

  • If session.SaveChanges fails, the email reservation is not rolled back automatically. It is your responsibility to do so.

  • The compare exchange value that was saved can be accessed from RQL in a query:

using (IDocumentSession session = store.OpenSession())
{
    var query = from u in session.Query<User>()
                where u.Id == RavenQuery.CmpXchg<string>("emails/ayende@ayende.com")
                select u;

    var q = session.Advanced
        .DocumentQuery<User>()
        .WhereEquals("Id", CmpXchg.Value("emails/ayende@ayende.com"));
}
from Users as s where id() == cmpxchg("emails/ayende@ayende.com")

Example II - Reserve a Shared Resource

  • Use compare exchange for a shared resource reservation.

  • The code also checks for clients who never release resources (i.e. due to failure) by using timeout.

private class SharedResource
{
    public DateTime? ReservedUntil { get; set; }
}

public void PrintWork() 
{
    // Try to get hold of the printer resource
    long reservationIndex = LockResource(store, "Printer/First-Floor", TimeSpan.FromMinutes(20));

    try
    {
        // Do some work for the duration that was set.
        // Don't exceed the duration, otherwise resource is available for someone else.
    }
    finally
    {
        ReleaseResource(store, "Printer/First-Floor", reservationIndex);
    }
}

public long LockResource(IDocumentStore store, string resourceName, TimeSpan duration)
{
    while (true)
    {
        DateTime now = DateTime.UtcNow;

        SharedResource resource = new SharedResource
        {
            ReservedUntil = now.Add(duration)
        };

        CompareExchangeResult<SharedResource> saveResult = store.Operations.Send(
                new PutCompareExchangeValueOperation<SharedResource>(resourceName, resource, 0));

        if (saveResult.Successful)
        {
            // resourceName wasn't present - we managed to reserve
            return saveResult.Index;
        }

        // At this point, Put operation failed - someone else owns the lock or lock time expired
        if (saveResult.Value.ReservedUntil < now)
        {
            // Time expired - Update the existing key with the new value
            CompareExchangeResult<SharedResource> takeLockWithTimeoutResult = store.Operations.Send(
                new PutCompareExchangeValueOperation<SharedResource>(resourceName, resource, saveResult.Index));

            if (takeLockWithTimeoutResult.Successful)
            {
                return takeLockWithTimeoutResult.Index;
            }
        }

        // Wait a little bit and retry
        Thread.Sleep(20);
    }
}

public void ReleaseResource(IDocumentStore store, string resourceName, long index)
{
    CompareExchangeResult<SharedResource> deleteResult
        = store.Operations.Send(new DeleteCompareExchangeValueOperation<SharedResource>(resourceName, index));

    // We have 2 options here:
    // deleteResult.Successful is true - we managed to release resource
    // deleteResult.Successful is false - someone else took the lock due to timeout 
}