diff --git a/src/SqlPersistence.PersistenceTests/PersistenceTestsConfiguration.cs b/src/SqlPersistence.PersistenceTests/PersistenceTestsConfiguration.cs index ddc13ef1c..d8f4ce558 100644 --- a/src/SqlPersistence.PersistenceTests/PersistenceTestsConfiguration.cs +++ b/src/SqlPersistence.PersistenceTests/PersistenceTestsConfiguration.cs @@ -175,7 +175,7 @@ static OutboxPersister CreateOutboxPersister(IConnectionManager connectionManage ISqlOutboxTransaction transactionFactory() { return transactionScopeMode - ? new TransactionScopeSqlOutboxTransaction(concurrencyControlStrategy, connectionManager, IsolationLevel.ReadCommitted) + ? new TransactionScopeSqlOutboxTransaction(concurrencyControlStrategy, connectionManager, IsolationLevel.ReadCommitted, TimeSpan.Zero) : new AdoNetSqlOutboxTransaction(concurrencyControlStrategy, connectionManager, System.Data.IsolationLevel.ReadCommitted); } diff --git a/src/SqlPersistence.Tests/Outbox/OutboxPersisterTests.cs b/src/SqlPersistence.Tests/Outbox/OutboxPersisterTests.cs index 4baf9ca16..be45f75ae 100644 --- a/src/SqlPersistence.Tests/Outbox/OutboxPersisterTests.cs +++ b/src/SqlPersistence.Tests/Outbox/OutboxPersisterTests.cs @@ -54,7 +54,7 @@ OutboxPersister Setup(string theSchema) } return transactionScope - ? new TransactionScopeSqlOutboxTransaction(behavior, connectionManager, IsolationLevel.ReadCommitted) + ? new TransactionScopeSqlOutboxTransaction(behavior, connectionManager, IsolationLevel.ReadCommitted, TimeSpan.Zero) : new AdoNetSqlOutboxTransaction(behavior, connectionManager, System.Data.IsolationLevel.ReadCommitted); }, cleanupBatchSize: 5); diff --git a/src/SqlPersistence/Outbox/SqlOutboxFeature.cs b/src/SqlPersistence/Outbox/SqlOutboxFeature.cs index e440dc0ed..5de4b9f82 100644 --- a/src/SqlPersistence/Outbox/SqlOutboxFeature.cs +++ b/src/SqlPersistence/Outbox/SqlOutboxFeature.cs @@ -28,6 +28,7 @@ protected override void Setup(FeatureConfigurationContext context) adoTransactionIsolationLevel = System.Data.IsolationLevel.ReadCommitted; } var transactionScopeIsolationLevel = context.Settings.GetOrDefault(TransactionScopeIsolationLevel); + var transactionScopeTimeout = context.Settings.GetOrDefault(TransactionScopeTimeout); var outboxCommands = OutboxCommandBuilder.Build(sqlDialect, tablePrefix); @@ -44,7 +45,7 @@ protected override void Setup(FeatureConfigurationContext context) ISqlOutboxTransaction transactionFactory() { return transactionScopeMode - ? (ISqlOutboxTransaction)new TransactionScopeSqlOutboxTransaction(concurrencyControlStrategy, connectionManager, transactionScopeIsolationLevel) + ? (ISqlOutboxTransaction)new TransactionScopeSqlOutboxTransaction(concurrencyControlStrategy, connectionManager, transactionScopeIsolationLevel, transactionScopeTimeout) : new AdoNetSqlOutboxTransaction(concurrencyControlStrategy, connectionManager, adoTransactionIsolationLevel); } @@ -87,4 +88,5 @@ ISqlOutboxTransaction transactionFactory() internal const string UseTransactionScope = "Persistence.Sql.Outbox.TransactionScopeMode"; internal const string AdoTransactionIsolationLevel = "Persistence.Sql.Outbox.AdoTransactionIsolationLevel"; internal const string TransactionScopeIsolationLevel = "Persistence.Sql.Outbox.TransactionScopeIsolationLevel"; + internal const string TransactionScopeTimeout = "Persistence.Sql.Outbox.TransactionScopeTimeout"; } \ No newline at end of file diff --git a/src/SqlPersistence/Outbox/SqlPersistenceOutboxSettingsExtensions.cs b/src/SqlPersistence/Outbox/SqlPersistenceOutboxSettingsExtensions.cs index 7429dd8cd..6ad18e99c 100644 --- a/src/SqlPersistence/Outbox/SqlPersistenceOutboxSettingsExtensions.cs +++ b/src/SqlPersistence/Outbox/SqlPersistenceOutboxSettingsExtensions.cs @@ -107,5 +107,23 @@ public static void UseTransactionScope(this OutboxSettings outboxSettings, Isola outboxSettings.GetSettings().Set(SqlOutboxFeature.UseTransactionScope, true); outboxSettings.GetSettings().Set(SqlOutboxFeature.TransactionScopeIsolationLevel, isolationLevel); } + + /// + /// Configures the outbox to use TransactionScope instead of SqlTransaction. This allows wrapping the + /// the outbox transaction (and synchronized storage session it manages) and other database transactions in a single scope - provided that + /// Distributed Transaction Coordinator (DTC) infrastructure is configured. + /// + /// Outbox settings. + /// Isolation level to use. Only levels Read Committed, Repeatable Read and Serializable are supported. + /// timeout period for the transaction. + public static void UseTransactionScope(this OutboxSettings outboxSettings, IsolationLevel isolationLevel, TimeSpan timeout) + { + if (timeout > TransactionManager.MaximumTimeout) + { + throw new Exception("Timeout requested is longer than the maximum value for this machine. Override using the maxTimeout setting of the system.transactions section in machine.config"); + } + outboxSettings.UseTransactionScope(isolationLevel); + outboxSettings.GetSettings().Set(SqlOutboxFeature.TransactionScopeTimeout, timeout); + } } } diff --git a/src/SqlPersistence/Outbox/TransactionScopeSqlOutboxTransaction.cs b/src/SqlPersistence/Outbox/TransactionScopeSqlOutboxTransaction.cs index c80555e7e..5317f99f7 100644 --- a/src/SqlPersistence/Outbox/TransactionScopeSqlOutboxTransaction.cs +++ b/src/SqlPersistence/Outbox/TransactionScopeSqlOutboxTransaction.cs @@ -1,4 +1,5 @@ -using System.Data.Common; +using System; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using System.Transactions; @@ -16,13 +17,15 @@ class TransactionScopeSqlOutboxTransaction : ISqlOutboxTransaction TransactionScope transactionScope; Transaction ambientTransaction; bool commit; + TimeSpan transactionTimeout; public TransactionScopeSqlOutboxTransaction(ConcurrencyControlStrategy concurrencyControlStrategy, - IConnectionManager connectionManager, IsolationLevel isolationLevel) + IConnectionManager connectionManager, IsolationLevel isolationLevel, TimeSpan transactionTimeout) { this.connectionManager = connectionManager; this.isolationLevel = isolationLevel; this.concurrencyControlStrategy = concurrencyControlStrategy; + this.transactionTimeout = transactionTimeout; } public DbTransaction Transaction => null; @@ -33,7 +36,8 @@ public void Prepare(ContextBag context) { var options = new TransactionOptions { - IsolationLevel = isolationLevel + IsolationLevel = isolationLevel, + Timeout = transactionTimeout // TimeSpan.Zero is default of `TransactionOptions.Timeout` }; transactionScope = new TransactionScope(TransactionScopeOption.RequiresNew, options, TransactionScopeAsyncFlowOption.Enabled);