One of the most critical things about persistence of sagas is proper concurrency control. Sagas guarantee business data consistency across long running processes using compensating actions. A failure in concurrency management that leads to creation of an extra instance of a saga instead of routing a message to an existing instance could lead to business data corruption.
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.
Starting a saga
Example exception:
NHibernate.Exceptions.GenericADOException: could not execute batch command.[SQL: SQL not available] ---> System.Data.SqlClient.SqlException: Violation of UNIQUE KEY constraint 'UQ__OrderSag__C3905BCE71EF212B'. Cannot insert duplicate key in object 'dbo.OrderSagaData'. The duplicate key value is (e87490ba-bb56-4693-9c0a-cf4f95736e06).
Updating or deleting saga data
No exceptions will be thrown. Conflicts cannot occur because the persistence uses pessimistic locking. Pessimistic locking is achieved by performing a SELECT ... FOR UPDATE, see NHibernate Chapter 12. Transactions And Concurrency.
Custom behavior
Explicit version
The RowVersion
attribute can be used to explicitly denote a property that should be used for optimistic concurrency control. An update will then compare against this single property instead of comparing it against the previous state of all properties which results in a more efficient comparison.
#pragma warning disable NSB0012 // Saga data classes should inherit ContainSagaData - [RowVersion] requires non-derived class
public class SagaDataWithRowVersion :
IContainSagaData
{
[RowVersion]
public virtual int MyVersion { get; set; }
public virtual string OriginalMessageId { get; set; }
public virtual string Originator { get; set; }
public virtual Guid Id { get; set; }
}
#pragma warning restore NSB0012 // Saga data classes should inherit ContainSagaData
That property will be included by NHibernate in the SELECT
and UPDATE
SQL statements causing a concurrency violation error to be raised in case of concurrent updates.
Marking a property with RowVersion
does not disable the pessimistic locking optimization. All it does is replace the default optimistic concurrency validation that depends on values of all columns with one that is based on that single explicit version column. To switch to pure optimistic concurrency adjust the locking strategy to Read
.
The RowVersion
attribute is not supported when used on derived classes. To specify a custom row version property, don't inherit saga data from the ContainSagaData
class; instead directly implement the IContainSagaData
interface.
In NServiceBus 7.7 and above, implementing IContainSagaData
directly will cause an analyzer warning, but since this is required to use [RowVersion]
, suppress the analyzer warning using #pragma warning disable NSB0012
.
In most cases where the saga data table is only ever accessed by the saga persister, it is advisable to use an explicit version because the UPDATE
SQL statement is much simpler and faster. The downside is that it does not detect concurrency violations if the data is updated by some external party that does not conform to the protocol i.e. does not bump the version field when doing updates. If such an external modification is possible, e.g. when different business process touches the same set of data, it is better to use the default optimistic concurrency validation strategy.
Adjusting the locking strategy
The LockMode
attribute can be used to override the default locking strategy.
[LockMode(LockModes.Read)]
public class SagaDataWithLockMode :
ContainSagaData
Customizing the optimistic concurrency handling
In order to customize or switch off optimistic concurrency handling, the optimistic-lock
NHibernate attribute has to be specified in a custom mapping. The custom mapping sample explains how to override the default mapping with a custom one.