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 = User(email=email)
with store.open_session() as sesion:
sesion.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 the session transaction,
# It is a cluster-wide reservation
cmp_xchg_result = store.operations.send(PutCompareExchangeValueOperation(f"emails/{email}", user.Id, 0))
if cmp_xchg_result.successful is False:
raise RuntimeError("Email is already in use")
# At this point we managed to reserve/save the user mail -
# The document can be saved in save_changes
sesion.save_changes()
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.save_changes
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 = sesion.query(object_type=User).where_equals("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:
def __init__(self, reserved_until: datetime = None):
self.reserved_until = reserved_until
def print_work() -> None:
# Try to get hold of the printer resource
reservation_index = lock_resource(store, "Printer/First-Floor", timedelta(minutes=20))
try:
...
# Do some work for the duration that was set
# Don't exceed the duration, otherwise resource is available for someone else
finally:
release_resource(store, "Printer/First-Floor", reservation_index)
def lock_resource(document_store: DocumentStore, resource_name: str, duration: timedelta):
while True:
now = datetime.utcnow()
resource = SharedResource(reserved_until=now + duration)
save_result = document_store.operations.send(
PutCompareExchangeValueOperation(resource_name, resource, 0)
)
if save_result.successful:
# resource_name wasn't present - we managed to reserve
return save_result.index
# At this point, Put operation failed - someone else owns the lock or lock time expired
if save_result.value.reserved_until < now:
# Time expired - Update the existing key with new value
take_lock_with_timeout_result = document_store.operations.send(
PutCompareExchangeValueOperation(resource_name, resource, save_result.index)
)
if take_lock_with_timeout_result.successful:
return take_lock_with_timeout_result.index
# Wait a little bit and retry
time.sleep(0.02)
def release_resource(store: DocumentStore, resource_name: str, index: int) -> None:
delete_result = store.operations.send(DeleteCompareExchangeValueOperation(resource_name, index))
# We have 2 options here:
# delete_result.successful is True - we managed to release resource
# delete_result.successful 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.CLUSTER_WIDE.
# 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.
# The reference document class
class UniquePhoneReference:
class PhoneReference:
def __init__(self, Id: str = None, company_id: str = None):
self.Id = Id
self.company_id = company_id
def main(self):
# A company document class that must be created with a unique 'Phone' field
new_company = Company(
name="companyName", phone="phoneNumber", contact=Contact(name="contactName", title="contactTitle")
)
def create_company_with_unique_phone(new_company: Company) -> None:
# Open a cluster-wide session in your document store
with store.open_session(
session_options=SessionOptions(transaction_mode=TransactionMode.CLUSTER_WIDE)
) as session:
# 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.
phone_ref_document = session.load(f"phones/{new_company.phone}")
if phone_ref_document is not None:
msg = f"Phone '{new_company.phone}' already exists, store the new entity"
raise RuntimeError(msg)
# If the new phone number doesn't already exist, store the new entity
session.store(new_company)
# Store a new reference document with the new phone value in its ID for future checks
session.store(
UniquePhoneReference.PhoneReference(company_id=new_company.Id),
f"phones/{new_company.phone}",
)
# May fail if called concurrently with the same phone number
session.save_changes()