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
-
Compare exchange items are created and managed with any of the following approaches:
- RavenDB creates and maintains Atomic Guards automatically in cluster-wide sessions to guarantee ACIDity across the cluster. (As of RavenDB version 5.2)
- Via Client API Operations
- In Cluster-Wide Sessions
- Using Studio
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.- This is not the case when using compare-exchange session methods
-
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:
-
Get the existing Key. The associated Value and Index are received.
-
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
}