This article covers various levels of consistency guarantees NServiceBus provides with regards to:
- receiving messages
- updating user data
- sending messages
It does not discuss the transaction isolation aspect. Transaction isolation applies only to the process of updating the user data, it does not affect the overall coordination and failure handling.
NServiceBus offers four levels of guarantees with regards to message processing. Levels availability depends on the selected transport. See also Transaction handling in Azure.
The implementation details for each transport are discussed in the dedicated documentation sections. They can be accessed by clicking the links with the transport name in the following table:
|Transaction scope (Distributed transaction)||Transport transaction - Sends atomic with Receive||Transport transaction - Receive Only||Unreliable (Transactions Disabled)|
|Azure Storage Queues||✖||✖||✔||✔|
|Azure Service Bus||✖||✔||✔||✔|
In this mode the transport receive operation is wrapped in a
TransactionScope. Other operations inside this scope, both sending messages and manipulating data, are guaranteed to be executed (eventually) as a whole or rolled back as a whole.
If required the transaction is escalated to a distributed one (following two-phase commit protocol). That depends on the transport and persistence choice, for example it is not needed when using SQL Server transport with NHibernate persistence both using the same database. In this case the ADO.NET driver guarantees that everything happens inside a single database transaction, ACID guarantees are held for the whole processing.
Transaction scope mode is enabled by default for the transports that support it (i.e. MSMQ and SQL Server transport). It can be enabled explicitly via
var transport = endpointConfiguration.UseTransport<MyTransport>(); transport.Transactions(TransportTransactionMode.TransactionScope);
In this mode handlers will execute inside the
TransactionScope created by the transport. This means that all the data updates are executed as a whole or rolled back as a whole.
Distributed transactions do not guarantee atomicity for an outside observer. For example outgoing messages might be dispatched and processed by some other endpoint before changes to the database get committed. However it is guaranteed that eventually all resources will sync up to reflect the outcome of the transaction.
In this mode the receive operation is wrapped in a transport's native transaction. This mode guarantees that the message is not permanently deleted from the incoming queue until at least one processing attempt (including storing user data and sending out messages) is finished successfully. See also recoverability for more details on retries.
Use the following code to use this mode:
var transport = endpointConfiguration.UseTransport<MyTransport>(); transport.Transactions(TransportTransactionMode.ReceiveOnly);
In this mode some (or all) handlers might get invoked multiple times and partial results might be visible:
- partial updates - where one handler succeeded updating its data but the other didn't
- partial sends - where some of the messages has been sent but others not
When using this mode all handlers must be idempotent, i.e. the result needs to be consistent from a business perspective even when the message is processed more than once.
Outbox section below for details on how NServiceBus can handle idempotency at the infrastructure level.
Some transports support enlisting outgoing operations in the current receive transaction. This prevents messages being sent to downstream endpoints during retries.
Use the following code to use this mode:
var transport = endpointConfiguration.UseTransport<MyTransport>(); transport.Transactions(TransportTransactionMode.SendsAtomicWithReceive);
This mode has the same consistency guarantees as the Receive Only mode, but additionally it prevents occurrence of ghost messages since all outgoing operations are atomic with the ongoing receive operation.
Disabling transactions is generally not recommended, because it might lead to the message loss. It might be considered if losing some messages is not problematic and if the messages get outdated quickly, e.g. when sending readings from sensors at regular intervals.
var transport = endpointConfiguration.UseTransport<MyTransport>(); transport.Transactions(TransportTransactionMode.None);
The Outbox feature provides idempotency at the infrastructure level and allows running in transport transaction mode while still getting the same semantics as Transaction scope mode.
When using the Outbox, any messages resulting from processing a given received message are not sent immediately but rather stored in the persistence database and pushed out after the handling logic is done. This mechanism ensures that the handling logic can only succeed once so there is no need to design for idempotency.
In this mode there is a risk of partial updates since one handler might succeed in updating business data while another handler fails. To avoid this configure NServiceBus to wrap all handlers in a
TransactionScope that will act as a unit of work and make sure that there is no partial updates. Use following code to enable a wrapping scope:
var unitOfWorkSettings = endpointConfiguration.UnitOfWork(); unitOfWorkSettings.WrapHandlersInATransactionScope();
The following options for transaction scopes used during message processing can be configured.
NServiceBus will by default use the
ReadCommitted isolation level. Change the isolation level using
var transport = endpointConfiguration.UseTransport<MyTransport>(); transport.Transactions(TransportTransactionMode.TransactionScope); transport.TransactionScopeOptions( isolationLevel: IsolationLevel.RepeatableRead);
NServiceBus will use the default transaction timeout of the machine the endpoint is running on.
Change the transaction timeout using
var transport = endpointConfiguration.UseTransport<MyTransport>(); transport.Transactions(TransportTransactionMode.TransactionScope); transport.TransactionScopeOptions( timeout: TimeSpan.FromSeconds(30));