Compare Exchange Overview


  • Compare-exchange items are cluster-wide key/value pairs where the key is a unique identifier.

  • Each compare-exchange item contains:

    • A key which is a unique string across the cluster.
    • A value which can be numbers, strings, arrays, or objects.
      Any value that can be represented as JSON is valid.
    • Metadata
    • Raft index - A version number which is modified on each change.
      Any change to the value or metadata changes the Raft index.
  • Creating and modifying a compare-exchange item is an atomic, thread-safe compare-and-swap interlocked compare-exchange operation.

    • The compare-exchange item is distributed to all nodes in a cluster-wide transaction so that a consistent, unique key is guaranteed cluster-wide.
  • To Ensure ACID Transactions RavenDB automatically creates Atomic Guards in cluster-wide transactions.

    • Cluster-wide transactions present a performance cost when compared to non-cluster-wide transactions. They prioritize consistency over performance to ensure ACIDity across the cluster.

*In this page:
* Using Compare-Exchange Items
* Transaction Scope for Compare-Exchange Operations
* Creating a Key
* Updating a Key
* Example I - Email Address Reservation
* Example II- Reserve a Shared Resource


Using Compare-Exchange Items

Why use compare-exchange items

  • Compare-exchange items can be used to coordinate work between sessions that are trying to modify a shared resource (such as a document) at the same time.

  • You can use compare-exchange items in various situations to protect or reserve a resource.
    (see API Compare-exchange examples).

    • If you create a compare-exchange key/value pair, you can decide what actions to implement when the Raft index increments. The Raft index increments as a result of a change in the compare-exchange value or metadata.

How compare-exchange items are managed


How Atomic Guard items work when protecting shared documents in cluster-wide sessions

  • Every time a document in a cluster-wide session is modified, RavenDB automatically creates or uses an associated compare-exchange item called Atomic Guard.
    • Whenever one session changes and saves a document, the Atomic Guard version changes. If other sessions loaded the document before the version changed, they will not be able to modify it.
    • A ConcurrencyException will be thrown if the Atomic Guard version was modified by another session.
  • If a ConcurrencyException is thrown:
    To ensure atomicity, if even one session transaction fails, the entire session will roll back.
    Be sure that your business logic is written so that if a concurrency exception is thrown, your code will re-execute the entire session.

Performance cost of cluster-wide sessions

Cluster-wide transactions are more expensive than node-local transactions due to Raft concensus checks.
People prefer a cluster-wide transaction when they prioritize consistency over performance and availability.
It ensures ACIDity across the cluster.

  • One way to protect ACID transactions without using cluster-wide sessions is to ensure that one node is responsible for writing on a specific database.
    • RavenDB node-local transactions are ACID on the local node, but if two nodes write concurrently on the same document, conlicts can occur.
    • By default to prevent conflicts, one node is responsible for all reads and writes.
      You can configure load balancing to fine-tune the settings to your needs.
    • To distribute work, you can set different nodes to be responsible for different sets of data.
      Learn more in the article Scaling Distributed Work in RavenDB.

Transaction Scope for Compare-Exchange Operations

Conventionally, compare-exchange session operations are used in cluster-wide sessions.
Since RavenDB 5.2, we automatically create and maintain Atomic Guards to guarantee cluster-wide session ACID transactions.

This article is about non-session-specific compare-exchange operations.

  • A non-session specific compare-exchange operation (described below) is performed on the document store level.
    It is therefore not part of the session transactions.

If a session fails

  • Even if written inside the session scope, a non-session compare exchange operation will be executed regardless of whether the session SaveChanges( ) succeeds or fails.

  • Thus, upon a session transaction failure, if you had a successful compare-exchange operation (as described below) inside the failed session block, it will not be rolled back automatically with the failed session.

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. This string can be up to 512 bytes.
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' email 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 
}