Skip to main content

Compare Exchange Overview


What compare-exchange items are

Compare-exchange items are key/value pairs where the key serves as a unique value across your database.

  • Each compare-exchange item contains:

    • A key - A unique string identifier in the database scope.
    • A value - Can be any value (a number, string, array, or any valid JSON object).
    • Metadata - Optional data that is associated with the compare-exchange item. Must be a valid JSON object.
      For example, the metadata can be used to set expiration time for the compare-exchange item.
      Learn more in compare-exchange expiration.
    • Raft index - The compare-exchange item's version.
      Any change to the value or metadata will increase this number.
  • Creating and modifying a compare-exchange item follows the same principle as the compare-and-swap operation in multi-threaded systems, but in RavenDB, this concept is applied to a distributed environment across multiple nodes instead of within a single multi-threaded process.
    These operations require cluster consensus to ensure consistency. Once consensus is reached, the compare-exchange items are distributed through the Raft algorithm to all nodes in the database group.


Ways to create and manage compare-exchange items

Compare exchange items can be created and managed using any of the following approaches:

  • Document Store Operations
    You can create and manage compare-exchange items using document store operations.
    For example, see Create items using a store operation.

  • Cluster-Wide Sessions
    You can create and manage compare-exchange items from within a Cluster-Wide session.
    For example, see Create items using a cluster-wide session.
    When using a cluster-wide session, the compare-exchange item is created as part of the cluster-wide transaction.
    Like any transaction, all operations either succeed as a group or are rolled back.
    If the transaction is not committed, the new compare-exchange item will not be stored on any node in the database group.

  • Atomic Guards
    When creating documents using a cluster-wide session, RavenDB automatically creates Atomic Guards,
    which are special compare-exchange items that guarantee ACID transactions.
    See Cluster-wide transaction vs. Single-node transaction for a session comparison overview.

  • Studio
    All compare-exchange items can also be managed from the Compare-Exchange view in the Studio:

    The compare-exchange view

    1. Open the Documents section in the Studio sidebar.
    2. Click on the Compare-Exchange tab.
    3. This is a compare-exchange item.
      In this view you can create, edit, and delete compare-exchange items.

Why compare-exchange items are not replicated to external databases

  • Each cluster defines its own policies and configurations, and should ideally have sole responsibility for managing its own documents. Read Consistency in a Globally Distributed System to learn more about why global database modeling is more efficient this way.

  • When creating a compare-exchange item, a Raft consensus is required from the nodes in the database group. Externally replicating such data is problematic because the target database may reside within a cluster that is in an unstable state where Raft decisions cannot be made. In such a state, the compare-exchange item will not be persisted in the target database.

  • Conflicts between documents that occur between two databases are resolved using the documents' change-vector. Compare-exchange conflicts cannot be resolved in the same way, as they lack a similar conflict resolution mechanism.

  • To ensure unique values between two databases without using compare-exchange items see Example III.

  • Learn more about Replication in RavenDB in Replication overview. For details about what is and what isn't replicated in What is Replicated.


Why not use regular documents to enforce uniqueness

  • You might consider storing a document with a predictable ID (for example, phones/123456) as a way to enforce uniqueness, and then checking for its existence before allowing another document to use the same value.

  • While this might work in a single-node setup or with external replication, it does not reliably enforce uniqueness in a clustered environment.

  • If a node was not part of the cluster when the document was created, it might not be aware of its existence when it comes back online. In such cases, attempting to load the document on this node may return null, leading to duplicate values being inserted.

  • To reliably enforce uniqueness across all cluster nodes, you must use compare-exchange items, which are designed for this purpose and ensure global consistency.


Example I - Email address reservation

The following example shows how to use compare-exchange to create documents with unique values.
The scope is within the database group on a single cluster.

string email = "user@example.com";

User user = new User
{
Email = email
};

using (IDocumentSession session = store.OpenSession())
{
session.Store(user);
// At this point, the user object has a document ID assigned by the session.

// Try to reserve the user email using a compare-exchange item.
// Note: This 'put compare-exchange operation' is not part of the session transaction,
// It is a separate, cluster-wide reservation.
CompareExchangeResult<string> cmpXchgResult
= store.Operations.Send(
// parameters passed to the operation:
// email - the unique key of the compare-exchange item
// user.Id - the value associated with the key
// 0 - pass `0` to ensure the item is created only if it doesn't already exist.
// If a compare-exchange item with the given key already exists, the operation will fail.
new PutCompareExchangeValueOperation<string>(email, user.Id, 0));

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

// At this point, the email has been successfully reserved/saved.
// We can now save the user document to the database.
session.SaveChanges();
}

Implications:

  • This compare-exchange item was created as an operation rather than with a cluster-wide session.
    Thus, if session.SaveChanges fails, then 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 in a query using the CmpXchg method:

 using (var session = store.OpenSession())
{
// Retrieve the user document that has the specified email:
var user = session.Query<User>()
// Access the compare-exchange value using the CmpXchg method:
.Where(x => x.Id == RavenQuery.CmpXchg<string>("ayende@ayende.com"))
.FirstOrDefault();
}

Example II - Reserve a shared resource

In the following example, we use compare-exchange to reserve a shared resource.
The scope is within the database group on a single cluster.

The code also checks for clients which 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 (TimeSpan.FromMinutes(20)).
//
// In a distributed system (unlike a multi-threaded app), a process may crash or exit unexpectedly
// without releasing the resource it reserved (i.e. never reaching the 'finally' block).
// This can leave the resource locked indefinitely.
//
// To prevent that, each reservation includes a timeout (TimeSpan.FromMinutes(20)).
// If the process fails or exits, the resource becomes available again once the timeout expires.
//
// Important: Ensure the work completes within the timeout period.
// If it runs longer, another client may acquire the same resource at the same time.
}
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> putResult = store.Operations.Send(
new PutCompareExchangeValueOperation<SharedResource>(resourceName, resource, 0));

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

// At this point, another process owns the resource.
// But if that process crashed and never released the resource, the reservation may have expired,
// so we can try to take the lock by overwriting the value using the current index.
if (putResult.Value.ReservedUntil < now)
{
// Time expired - Update the existing key with the new value
CompareExchangeResult<SharedResource> takeLockWithTimeoutResult = store.Operations.Send(
new PutCompareExchangeValueOperation<SharedResource>(
resourceName, resource, putResult.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
}