Compare Exchange Overview
-
Compare Exchange items are key/value pairs where the key is unique across your database.
-
Compare-exchange operations require cluster consensus to ensure consistency across all nodes.
Once a consensus is reached, the compare-exchange items are distributed through the Raft algorithm to all nodes in the database group. -
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.
-
Compare-exchange items are not replicated externally to other databases.
-
In this page:
- What Compare Exchange Items Are
- Creating and Managing Compare-Exchange Items
- Why Compare-Exchange Items are Not Replicated to External Databases
- Example I - Email Address Reservation
- Example II - Reserve a Shared Resource
- Example III - Ensuring Unique Values without Using Compare Exchange
What Compare Exchange Items Are
Compare Exchange items are key/value pairs where the key servers 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 object (a number, string, array, or any valid JSON object).
-
Metadata - 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.
- For example, the metadata can be used to set expiration time for the compare-exchange item.
- 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 is an atomic, thread-safe compare-and-swap interlocked compare-exchange operation.
Creating and Managing Compare-Exchange Items
Compare exchange items are created and managed with any of the following approaches:
-
Document Store Operations
You can manage a compare-exchange item as an Operation on the document store.
This can be done within or outside of a session (cluster-wide or single-node session).- When inside a session:
If the session fails, the compare-exchange operation can still succeed because store Operations do not rely on the success of the session.
You will need to delete the compare-exchange item explicitly upon session failure if you don't want the compare-exchange item to persist.
- When inside a session:
-
Cluster-Wide Sessions
You can manage a compare-exchange item from inside a Cluster-Wide session.
If the session fails, the compare-exchange item creation also fails.
None of the nodes in the group will have the new compare-exchange item. -
Atomic Guards
When creating documents using a cluster-wide session RavenDB automatically creates Atomic Guards,
which are compare-exchange items that guarantee ACID transactions.
See Cluster-wide vs. Single-node for a session comparision overview. -
Studio
Compare-exchange items can be created from the Studio as well.
Why Compare-Exchange Items are Not Replicated to External Databases
-
Each cluster defines its 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 as 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 solved with the help of the documents Change-Vector. Compare-exchange conflicts cannot be handled properly as they do not have a similar mechanism to resolve conflicts.
-
To ensure unique values between two databases without using compare-exchange items see Example III.
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.
Compare-exchange items are not externally replicated to other databases.
To establish uniqueness without using compare-exchange see Example III.
$email = "user@example.com";
$user = new User();
$user->setEmail($email);
$session = $store->openSession();
try {
$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
/** @var CompareExchangeResult $cmpXchgResult */
$cmpXchgResult = $store->operations()->send(new PutCompareExchangeValueOperation("emails/" . $email, $user->getId(), 0));
if (!$cmpXchgResult->isSuccessful()) {
throw new RuntimeException("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();
} finally {
$session->close();
}
Implications:
-
The
User
object is saved as a document, hence it can be indexed, queried, etc. -
This compare-exchange item was created as an operation rather than with a cluster-wide session.
Thus, ifsession.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
CmpXchg
:
$query = $session->advanced()->rawQuery(User::class, "from Users as s where id() == cmpxchg(\"emails/ayende@ayende.com\")") ->toList();
$q = $session->advanced() ->documentQuery(User::class) ->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
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.
class SharedResource
{
private ?DateTime $reservedUntil = null;
public function getReservedUntil(): ?DateTime
{
return $this->reservedUntil;
}
public function setReservedUntil(?DateTime $reservedUntil): void
{
$this->reservedUntil = $reservedUntil;
}
}
class CompareExchangeSharedResource
{
private ?DocumentStore $store = null;
public function printWork(): void
{
// Try to get hold of the printer resource
$reservationIndex = $this->lockResource($this->store, "Printer/First-Floor", Duration::ofMinutes(20));
try {
// Do some work for the duration that was set.
// Don't exceed the duration, otherwise resource is available for someone else.
} finally {
$this->releaseResource($this->store, "Printer/First-Floor", $reservationIndex);
}
}
/** throws InterruptedException */
public function lockResource(DocumentStoreInterface $store, ?string $resourceName, Duration $duration): int
{
while (true) {
$now = new DateTime();
$resource = new SharedResource();
$resource->setReservedUntil($now->add($duration->toDateInterval()));
/** @var CompareExchangeResult<SharedResource> $saveResult */
$saveResult = $store->operations()->send(
new PutCompareExchangeValueOperation($resourceName, $resource, 0));
if ($saveResult->isSuccessful()) {
// resourceName wasn't present - we managed to reserve
return $saveResult->getIndex();
}
// At this point, Put operation failed - someone else owns the lock or lock time expired
if ($saveResult->getValue()->getReservedUntil() < $now) {
// Time expired - Update the existing key with the new value
/** @var CompareExchangeResult<SharedResource> takeLockWithTimeoutResult */
$takeLockWithTimeoutResult = $store->operations()->send(
new PutCompareExchangeValueOperation($resourceName, $resource, $saveResult->getIndex()));
if ($takeLockWithTimeoutResult->isSuccessful()) {
return $takeLockWithTimeoutResult->getIndex();
}
}
// Wait a little bit and retry
usleep(20000);
}
}
public function releaseResource(DocumentStoreInterface $store, ?string $resourceName, int $index): void
{
$deleteResult = $store->operations()->send(
new DeleteCompareExchangeValueOperation(SharedResource::class, $resourceName, $index)
);
// We have 2 options here:
// $deleteResult->isSuccessful is true - we managed to release resource
// $deleteResult->isSuccessful is false - someone else took the lock due to timeout
}
}
Example III - Ensuring Unique Values without Using Compare Exchange
Unique values can also be ensured without using compare-exchange.
The below example shows how to achieve that by using reference documents.
The reference documents' IDs will contain the unique values instead of the compare-exchange items.
Using reference documents is especially useful when External Replication
is defined between two databases that need to be synced with unique values.
The reference documents will replicate to the destination database,
as opposed to compare-exchange items, which are not externally replicated.
Sessions which process fields that must be unique should be set to TransactionMode::clusterWide().
// When you create documents that must contain a unique value such as a phone or email, etc.,
// you can create reference documents that will have that unique value in their IDs.
// To know if a value already exists, all you need to do is check whether a reference document with such ID exists.
public
class PhoneReference
{
public ?string $id = null;
public ?string $companyId = null;
public function getId(): ?string
{
return $this->id;
}
public function setId(?string $id): void
{
$this->id = $id;
}
public function getCompanyId(): ?string
{
return $this->companyId;
}
public function setCompanyId(?string $companyId): void
{
$this->companyId = $companyId;
}
}
// The reference document class
class UniquePhoneReference
{
public function sample(): void
{
// A company document class that must be created with a unique 'Phone' field
$newCompany = new Company();
$newCompany->setName("companyName");
$newCompany->setPhone("phoneNumber");
$newContact = new Contact();
$newContact->setName("contactName");
$newContact->setTitle("contactTitle");
$newCompany->setContact($newContact);
$this->createCompanyWithUniquePhone($newCompany);
}
public function createCompanyWithUniquePhone(Company $newCompany): void
{
// Open a cluster-wide session in your document store
$sessionOptions = new SessionOptions();
$sessionOptions->setTransactionMode(TransactionMode::clusterWide());
$session = DocumentStoreHolder::getStore()->openSession($sessionOptions);
try {
// Check whether the new company phone already exists
// by checking if there is already a reference document that has the new phone in its ID.
$phoneRefDocument = $session->load(PhoneReference::class, "phones/" . $newCompany->getPhone());
if ($phoneRefDocument != null) {
$msg = "Phone '" . $newCompany->getPhone() . "' already exists in ID: " . $phoneRefDocument->getCompanyId();
throw new ConcurrencyException($msg);
}
// If the new phone number doesn't already exist, store the new entity
$session->store($newCompany);
// Store a new reference document with the new phone value in its ID for future checks.
$newPhoneReference = new PhoneReference();
$newPhoneReference->setCompanyId($newCompany->getId());
$session->store($newPhoneReference, "phones/" . $newCompany->getPhone());
// May fail if called concurrently with the same phone number
$session->saveChanges();
} finally {
$session->close();
}
}
}