RavenDB Persistence Saga Concurrency

Component: RavenDB Persistence
NuGet Package NServiceBus.RavenDB (6.5)
Target NServiceBus Version: 7.x
RavenDB's implementation of distributed transactions contains a bug that could cause an endpoint, in certain (rare) conditions, to lose data. If RavenDB is configured to enlist in distributed transactions, read DTC not supported for RavenDB Persistence.
Using RavenDB version 4 and higher in a cluster configuration with multiple nodes is not supported. For more information, read cluster configuration with multiple nodes not supported.

Default behavior

When simultaneously handling messages, conflicts may occur. See below for examples of the exceptions which are thrown. Saga concurrency explains how these conflicts are handled, and contains guidance for high-load scenarios.

Creating saga data

Example exception:

Raven.Client.Exceptions.ConcurrencyException: Document OrderSagaData/OrderId/316414b3-07f1-40ec-00db-022a4140d517 has change vector A:2-u2LvKAFZTE+972x2hp1gTg, but Put was called with expecting new document. Optimistic concurrency violation, transaction will be aborted.

Updating or deleting saga data

By default, RavenDB persistence uses optimistic concurrency control when updating or deleting saga data, though starting with NServiceBus.RavenDB version 6.5, it's possible to configure the persister to use pessimistic locking. See later in this document for how to do this.

When a message handler does not change saga data, the RavenDB client will not attempt to write the associated document to storage. If a consistency check is required, a property value must be changed. For example, a counter property may be incremented.

Example exception:

Raven.Client.Exceptions.ConcurrencyException: Document OrderSagaDatas/f23921c9-7b53-455d-89be-aad200d98741 has change vector A:93-u2LvKAFZTE+972x2hp1gTg, but Put was called with change vector A:90-u2LvKAFZTE+972x2hp1gTg. Optimistic concurrency violation, transaction will be aborted.
This means that the relevant Handle method on the saga will be invoked, even though the message might be later rolled back. Hence it is important to ensure not to perform any work in saga handlers that can't roll back together with the message. This also means that should there be high levels of concurrency there will be N-1 rollbacks where N is the number of concurrent messages. This can cause throughput issues and might require design changes.

Sagas concurrency control

By default NServiceBus.RavenDB uses optimistic concurrency control. Pessimistic locking can be enabled with the following API:

var sagasConfig = endpointConfiguration.UsePersistence<RavenDBPersistence>()
    .Sagas();
sagasConfig.UsePessimisticLocking();
Starting with NServiceBus.RavenDB version 6.5, pessimistic locking is the default option for concurrency control.

Pessimistic locking internals

RavenDB does not support pessimistic locking natively and the persister uses a dedicated RavenDB document to enforce exclusive saga updates. In some scenarios, this can lead to an increase in I/O roundtrips, especially if many instances are competing for the same saga. However, in high concurrency scenarios, the overhead is still smaller than the cost of message retry that would be caused by optimistic concurrency failures.

It is recommended to choose pessimistic concurrency over optimistic concurrency whenever a saga is experiencing a high number of optimistic concurrency control errors.

Pessimistic concurrency control settings

The pessimistic concurrency control can be customized using the options mentioned below.

Pessimistic lease lock time

By default, the persister locks a saga data document for 60 seconds. Although it is not recommended to have sagas execute long-running logic, in some scenarios it might be required to increase the lease duration. The lease duration can be adjusted using the following API:

var sagasConfig = endpointConfiguration.UsePersistence<RavenDBPersistence>()
    .Sagas();
sagasConfig.SetPessimisticLeaseLockTime(TimeSpan.FromMinutes(2));

Pessimistic lease lock acquisition timeout

By default, the persister waits 60 seconds to acquire the lock. The value can be adjusted using the following API:

var sagasConfig = endpointConfiguration.UsePersistence<RavenDBPersistence>()
    .Sagas();
sagasConfig.SetPessimisticLeaseLockAcquisitionTimeout(TimeSpan.FromSeconds(15));

Pessimistic lease lock acquisition maximum refresh delay

To prevent request synchronization, the persister randomizes the interval between lock acquisition requests. By default, the interval has a value between 0 and 20 milliseconds. The 20ms default upper limit can be changed to any value between 0 and 1000 milliseconds using the following API:

var sagasConfig = endpointConfiguration.UsePersistence<RavenDBPersistence>()
    .Sagas();
sagasConfig.SetPessimisticLeaseLockAcquisitionMaximumRefreshDelay(TimeSpan.FromMilliseconds(500));

Related Articles

  • Saga concurrency
    NServiceBus ensures consistency between saga state and messaging.

Last modified