Transport transactions

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 which only applies to the process of updating the user data and does not affect the overall coordination and failure handling.

Transactions

Based on transaction handling mode, NServiceBus offers three levels of guarantees with regards to message processing. The levels available depends on the capability of the selected transport.

Transaction scope (Distributed transaction)

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.

Depending on transport the transaction is escalated to a distributed one (following two-phase commit protocol) when required. For example, this is not required when using SQL Server transport with NHibernate persistence both targeted at 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.

MSMQ will escalate to a distributed transaction right away since it doesn't support promotable transaction enlistments.

Transaction scope mode is enabled by default for the transports that support it. It can be enabled explicitly via

Configure.Transactions.Enable().Advanced(x => x.EnableDistributedTransactions());
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.Transactions()
    .Enable()
    .EnableDistributedTransactions();
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.UseTransport<MyTransport>()
    .Transactions(TransportTransactionMode.TransactionScope);

Consistency guarantees

In this mode handlers will execute inside of 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. Changes to the database might be visible before the messages on the queue or vice-versa, but they are guaranteed to eventually all sync up to reflect the outcome of the transaction.

This requires the selected storage to support participating in distributed transactions.

Transport transaction - Receive Only

In this mode the receive operation is wrapped in a transport 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 the errors section for more details on retries.

Use the following code to use this mode:

Configure.Transactions.Advanced(x => x.DisableDistributedTransactions());
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.Transactions()
    .DisableDistributedTransactions();
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.UseTransport<MyTransport>()
    .Transactions(TransportTransactionMode.ReceiveOnly);
Prior to Version 6 receive only mode couldn't be requested for transports supporting the atomic sends with receive mode (see below)

Consistency guarantees

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

To handle this make sure to write code that is consistent from a business perspective even though its executed more than once. In other words all handlers must be idempotent.

See the Outbox section below for details on how NServiceBus can handle idempotency at the infrastructure level.

Version 5 and below didn't have batched dispatch and this meant that messages could be sent out without a matching update to business data depending the order of statements. This is called ghost messages. To avoid this make sure to perform all bus operations after any modification to business data. When reviewing the code remember that there can be multiple handlers for a given message and that handlers have an order of execution.

Transport transaction - Sends atomic with Receive

Some transports support enlisting outgoing operations in the current receive transaction. This prevents messages being sent to downstream endpoints during retries. Currently only the MSMQ and SQL Server transports support this type of transaction mode.

Use the following code to use this mode:

Configure.Transactions.Advanced(x => x.DisableDistributedTransactions());
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.Transactions()
    .DisableDistributedTransactions();
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.UseTransport<MyTransport>()
    .Transactions(TransportTransactionMode.SendsAtomicWithReceive);

Consistency guarantees

This mode has the same consistency guarantees as the Receive Only mode mentioned above with the difference that ghost messages are prevented since all outgoing operations are atomic with the ongoing receive operation.

Unreliable (Transactions Disabled)

In this mode the transport doesn't wrap the receive operation in any kind of transaction. Should the message fail to process it will be moved straight to the error queue. There will be no first level or second level retries since those features rely on transport level transactions.

If there is a critical failure, including system or endpoint crash, the message is permanently lost since it's received with no transaction.
In Version 5 and below, when transactions are disabled, no retries will be performed and messages will not be forwarded to the error queue in the event of any failure.
Configure configure = Configure.With();
configure.DontUseTransactions();
Configure.Transactions.Disable();
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.Transactions()
    .Disable();
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.UseTransport<MyTransport>()
    .Transactions(TransportTransactionMode.None);

Outbox

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.

Outbox data needs to be stored in the same database as business data to achieve the idempotency mentioned above.

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.

Avoiding partial updates

In this mode there is a risk for partial updates since one handler might succeed in updating business data while other won't. 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:

BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.Transactions()
    .DisableDistributedTransactions()
    .WrapHandlersExecutionInATransactionScope();
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.UnitOfWork()
    .WrapHandlersInATransactionScope();
This requires the selected storage to support enlisting in transaction scopes.
This might escalate to a distributed transaction if data in different databases are updated.
This API must not be used in combination with transports running in transaction scope mode. Starting from version 6, wrapping handlers in a TransactionScope in such a situation throws an exception.

Controlling transaction scope options

The following options for transaction scopes used during message processing can be configured.

Starting from version 6 isolation level and timeout for transaction scopes are also configured at the transport level.

Isolation level

NServiceBus will by default use the ReadCommitted isolation level.

Version 3 and below used the default isolation level of .Net which is Serializable.

Change the isolation level using

Configure configure = Configure.With();
configure.IsolationLevel(IsolationLevel.RepeatableRead);
Configure.Transactions.Advanced(x => x.IsolationLevel(IsolationLevel.RepeatableRead));
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.Transactions()
    .IsolationLevel(IsolationLevel.RepeatableRead);
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.UseTransport<MyTransport>()
    .Transactions(TransportTransactionMode.TransactionScope)
    .TransactionScopeOptions(isolationLevel: IsolationLevel.RepeatableRead);

Transaction timeout

NServiceBus will use the default transaction timeout of the machine the endpoint is running on.

Change the transaction timeout using

Configure configure = Configure.With();
configure.TransactionTimeout(TimeSpan.FromSeconds(30));
Configure.Transactions.Advanced(x => x.DefaultTimeout(TimeSpan.FromSeconds(30)));
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.Transactions()
    .DefaultTimeout(TimeSpan.FromSeconds(30));
BusConfiguration busConfiguration = new BusConfiguration();
busConfiguration.UseTransport<MyTransport>()
    .Transactions(TransportTransactionMode.TransactionScope)
    .TransactionScopeOptions(timeout: TimeSpan.FromSeconds(30));

Or via .config file using a example DefaultSettingsSection.


Last modified 2016-01-11 14:00:26Z