The outbox feature requires persistent storage to store outgoing messages and enable deduplication.
Table
To track duplicate messages, NHibernate Persistence requires an OutboxRecord table.
Use the following API to customize the table name and schema:
var persistence = endpointConfiguration.UsePersistence<NHibernatePersistence>();
persistence.CustomizeOutboxTableName(
outboxTableName: "MyEndpointOutbox",
outboxSchemaName: "MySchema");
Concurrency control
By default, the outbox uses optimistic concurrency control; when two copies of the same message arrive, the endpoint may process both concurrently. After the message handlers finish, both processing attempts try to insert an outbox record in the transaction that contains the application state change. One transaction will succeed, and the other will fail with a unique index constraint violation. When the failed message is processed again, the endpoint discards it as a duplicate.
The application state change is applied only once since the other attempt is rolled back, but the message handlers still run twice. Non-transactional side effects, e.g. sending an email, may therefore occur more than once.
Pessimistic concurrency control
Enable pessimistic concurrency control using the following API:
var outboxSettings = endpointConfiguration.EnableOutbox();
outboxSettings.UsePessimisticConcurrencyControl();
In pessimistic mode, the outbox record is inserted before the handlers run. With a database that locks inserted rows, only one processing attempt can run the message handlers. The attempt processing the duplicate waits for the database lock. After the first attempt commits, the duplicate insert fails, and the handlers do not run for the duplicate.
The trade-off is that each message processing attempt requires an additional round trip to the database.
Pessimistic mode depends on how the database locks inserted rows. Consult the database documentation to determine which transaction isolation levels support this mode.
Pessimistic mode does not guarantee that message handling logic runs exactly once. Errors that cause retries can still duplicate non-transactional side effects, such as sending an email.
Transactions
By default, the outbox uses an ADO.NET transaction through NHibernate's ITransaction abstraction. This mode is appropriate for most scenarios.
Transaction Scope
When an outbox transaction must span multiple databases, enable TransactionScope support:
var outboxSettings = endpointConfiguration.EnableOutbox();
outboxSettings.UseTransactionScope();
In this mode, NHibernate Persistence creates a TransactionScope around the entire message processing attempt. Within that scope, it opens a session that is used for:
- Storing the outbox record.
- Persisting application state changes made through
SynchronizedStorageSession.
Message handlers can also open NHibernate sessions or database connections. When the database supports transactions managed by Microsoft Distributed Transaction Coordinator (MS DTC), enlisting multiple connections escalates the transaction to a distributed transaction. Examples of supported databases include SQL Server, Oracle, and PostgreSQL.
TransactionScope mode is primarily useful in legacy scenarios, e.g. when migrating from MSMQ to a transport that does not support distributed transactions. The outbox provides consistency in place of distributed transactions between the transport and the database. If the existing database cannot be modified to add the outbox table, place the table in a separate database and use a distributed transaction between the two databases.
Customizing the transaction isolation level
Use the following API to configure the transaction isolation level for outbox operations:
var outboxSettings = endpointConfiguration.EnableOutbox();
outboxSettings.UseTransactionScope();
outboxSettings.TransactionIsolationLevel(IsolationLevel.ReadCommitted);
The default isolation level is Serializable. The Chaos, ReadUncommitted, Snapshot, and Unspecified isolation levels are not supported. The outbox uses pessimistic locking to prevent concurrent duplicate message processing.
Customizing outbox record persistence
By default, NHibernate Persistence maps outbox records as follows:
- The table has an auto-incremented integer primary key.
- The
MessageIdcolumn has a unique index. - The
DispatchedandDispatchedAtcolumns have indexes.
Use the following API to map outbox data differently:
var persistence = endpointConfiguration.UsePersistence<NHibernatePersistence>();
persistence.UseOutboxRecord<MyOutboxRecord, MyOutboxRecordMapping>();
public class MyOutboxRecord :
IOutboxRecord
{
public virtual string MessageId { get; set; }
public virtual bool Dispatched { get; set; }
public virtual DateTime? DispatchedAt { get; set; }
public virtual string TransportOperations { get; set; }
}
public class MyOutboxRecordMapping :
ClassMapping<MyOutboxRecord>
{
public MyOutboxRecordMapping()
{
Table("MyOutboxTable");
Id(
idProperty: record => record.MessageId,
idMapper: mapper => mapper.Generator(Generators.Assigned));
Property(
property: record => record.Dispatched,
mapping: mapper =>
{
mapper.Column(c => c.NotNullable(true));
mapper.Index("OutboxRecord_Dispatched_Idx");
});
Property(
property: record => record.DispatchedAt,
mapping: pm => pm.Index("OutboxRecord_DispatchedAt_Idx"));
Property(
property: record => record.TransportOperations,
mapping: mapper => mapper.Type(NHibernateUtil.StringClob));
}
}
When using a custom mapping, the following characteristics of the default mapping must be preserved:
- Values in the
MessageIdcolumn must be unique. Attempting to insert a duplicate value must cause an exception. - Queries using the
DispatchedandDispatchedAtcolumns must be efficient. The cleanup process uses these columns to remove outdated records.
Deduplication record lifespan
By default, NHibernate Persistence keeps deduplication records for seven days and checks for outdated records every minute.
Specify different values in the configuration file using timestamp strings:
<appSettings>
<add key="NServiceBus/Outbox/NHibernate/TimeToKeepDeduplicationData"
value="7.00:00:00" />
<add key="NServiceBus/Outbox/NHibernate/FrequencyToRunDeduplicationDataCleanup"
value="00:01:00" />
</appSettings>
To disable the cleanup task, set the NServiceBus/ app setting to -00:00:00.. This value represents -1 millisecond and is equivalent to Timeout.. Disabling cleanup on the majority of instances avoids competition when an endpoint is scaled out.
Run the cleanup task on only one NServiceBus endpoint instance per database. For the most efficient cleanup, disable the task on all other endpoint instances that use the database.