Transport Transactions

Component: NServiceBus
NuGet Package NServiceBus (4.x)
Standard support for version 4.x of NServiceBus has expired. For more information see our Support Policy.

This article covers various levels of consistency guarantees 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.

Transactions

Four levels of guarantees with regards to message processing are offered. A levels availability depends on the selected transport.

Transaction levels supported by NServiceBus transports

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 transactionUnreliable (Transactions Disabled)
MSMQ
SQL Server
RabbitMQ
Azure Storage Queues
Azure Service Bus

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.

If required, the transaction is escalated to a distributed transaction (following two-phase commit protocol coordinated by MS DTC) if both the transport and the persistence support it. A fully distributed transaction is not always required, for example when using SQL Server transport with SQL persistence, both using the same database connection string. In this case the ADO.NET driver guarantees that everything happens inside a single database transaction and 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 (i.e. MSMQ and SQL Server transport). It can be enabled explicitly via

var transactions = Configure.Transactions;
transactions.Enable();
transactions.Advanced(
    action: advancedSettings =>
    {
        advancedSettings.EnableDistributedTransactions();
    });
Microsoft SQL Server does not support DTC transactions in all deployment models (such as database mirroring or Always On configurations) and support differs between versions of SQL Server. See Transactions - Always On availability groups and Database Mirroring for more information.

Atomicity and consistency guarantees

In this mode handlers will execute inside a TransactionScope created by the transport. This means that all the data updates and queue operations are all committed or all rolled back.

A distributed transaction between the queueing system and the persistent storage guarantees atomic commits but guarantees only eventual consistency.

Consider a system using MSMQ transport and RavenDB persistence implementing the following message exchange scenario with a saga that models a simple order lifecycle:

  1. OrderSaga receives a StartOrder message
  2. New OrderSagaData instance is created and stored in RavenDB.
  3. OrderSaga sends VerifyPayment message to PaymentService.
  4. NServiceBus completes the distributed transaction and the DTC instructs MSMQ and RavenDB resource managers to commit their local transactions.
  5. StartOrder message is removed from the input queue and VerifyPayment is immediately sent to PaymentService.
  6. RavenDB acknowledges the transaction commit and begins writing OrderSagaData to disk.
  7. PaymentService receives VerifyPayment message and immediately responds with a CompleteOrder message to the originating OrderSaga.
  8. OrderSaga receives the CompleteOrder message and attempts to complete the saga.
  9. OrderSaga queries RavenDB to find the OrderSagaData instance to complete.
  10. RavenDB has not finished writing OrderSagaData to disk and returns an empty result set.
  11. OrderSaga fails to complete.

In the example above the TransactionScope guarantees atomicity for the OrderSaga: consuming the incoming StartOrder message, storing OrderSagaData in RavenDB and sending the outgoing VerifyPayment message are committed as one atomic operation. The saga data may not be immediately available for reading even though the incoming message has already been processed. OrderSaga is thus only eventually consistent. The CompleteOrder message needs to be retried until RavenDB successfully returns an OrderSagaData instance.

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

Transport 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:

Configure.Transactions.Advanced(
    action: advancedSettings =>
    {
        advancedSettings.DisableDistributedTransactions();
    });

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

When using this mode all handlers must be idempotent. In other words the result needs to be consistent from a business perspective even when the message is processed more than once.

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

Not all the transport support batched dispatch and this means that messages could be sent out without a matching update to business data, depending on the order in which statements were executed. Such messages are called ghost messages. To avoid this situation make sure to perform all bus operations only after modifications to business data. When reviewing the code remember that there can be multiple handlers for a given message and that handlers are executed in a certain order.

Unreliable (Transactions Disabled)

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.

In this mode, when encountering a critical failure such as system or endpoint crash, the message is permanently lost.
var transactions = Configure.Transactions;
transactions.Disable();
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 and the message will be permanently lost.

Avoiding partial updates

In transaction modes lower than TransactionScope there is a risk of partial updates because 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 transactions = Configure.Transactions;
transactions.Enable();
transactions.Advanced(
    action: advancedSettings =>
    {
        advancedSettings.DisableDistributedTransactions();
        advancedSettings.WrapHandlersExecutionInATransactionScope();
    });
This requires that all the data stores used by the handler support enlisting in a distributed transaction (e.g. SQL Server), including the saga store when using sagas.
This might escalate to a distributed transaction if data in different databases are updated.
This API has no effect when used in combination with transports running in a transaction scope mode.

Controlling transaction scope options

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

Isolation level

NServiceBus will by default use the ReadCommitted isolation level. Change the isolation level using

var transactions = Configure.Transactions;
transactions.Advanced(
    action: advancedSettings =>
    {
        advancedSettings.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

var transactions = Configure.Transactions;
transactions.Advanced(
    action: advancedSettings =>
    {
        advancedSettings.DefaultTimeout(TimeSpan.FromSeconds(30));
    });

Via a config file using a the Timeout property of the DefaultSettingsSection.

Related Articles


Last modified