예제 #1
0
        public static async Task <ISqlTransactionalOutboxProcessingResults <Guid> > ProcessPendingOutboxItemsAsync(
            this SqlConnection sqlConnection,
            ISqlTransactionalOutboxPublisher <Guid> outboxPublisher,
            OutboxProcessingOptions processingOptions,
            bool throwExceptionOnFailure = false
            )
        {
            sqlConnection.AssertSqlConnectionIsValid();
            outboxPublisher.AssertNotNull(nameof(outboxPublisher));
            processingOptions.AssertNotNull(nameof(processingOptions));

            await using var outboxTransaction = (SqlTransaction)(await sqlConnection.BeginTransactionAsync().ConfigureAwait(false));
            try
            {
                var results = await outboxTransaction
                              .ProcessPendingOutboxItemsAsync(
                    outboxPublisher : outboxPublisher,
                    processingOptions : processingOptions,
                    throwExceptionOnFailure : throwExceptionOnFailure
                    )
                              .ConfigureAwait(false);

                await outboxTransaction.CommitAsync().ConfigureAwait(false);

                return(results);
            }
            catch (Exception)
            {
                await outboxTransaction.RollbackAsync().ConfigureAwait(false);

                throw;
            }
        }
 public AsyncThreadOutboxProcessingAgent(
     TimeSpan processingIntervalTimeSpan,
     string sqlConnectionString,
     ISqlTransactionalOutboxPublisher <Guid> outboxPublisher,
     OutboxProcessingOptions outboxProcessingOptions = null
     ) : this(processingIntervalTimeSpan, TimeSpan.Zero, sqlConnectionString, outboxPublisher, outboxProcessingOptions)
 {
 }
 public AsyncThreadOutboxProcessingAgent(
     TimeSpan processingIntervalTimeSpan,
     TimeSpan historyToKeepTimeSpan,
     string sqlConnectionString,
     ISqlTransactionalOutboxPublisher <Guid> outboxPublisher,
     OutboxProcessingOptions outboxProcessingOptions = null
     )
 {
     this.ProcessingIntervalTimespan              = processingIntervalTimeSpan.AssertNotNull(nameof(processingIntervalTimeSpan));
     this.HistoryToKeepTimespan                   = historyToKeepTimeSpan.AssertNotNull(nameof(historyToKeepTimeSpan));
     this.SqlConnectionString                     = sqlConnectionString.AssertNotNullOrWhiteSpace(nameof(sqlConnectionString));
     this.SqlTransactionalOutboxPublisher         = outboxPublisher.AssertNotNull(nameof(outboxPublisher));
     this.SqlTransactionalOutboxProcessingOptions = outboxProcessingOptions ?? OutboxProcessingOptions.DefaultOutboxProcessingOptions;
 }
        public static async Task <ISqlTransactionalOutboxProcessingResults <Guid> > ProcessPendingOutboxItemsAsync(
            this SqlConnection sqlConnection,
            ISqlTransactionalOutboxPublisher <Guid> outboxPublisher,
            OutboxProcessingOptions processingOptions,
            bool throwExceptionOnFailure = false
            )
        {
            sqlConnection.AssertSqlConnectionIsValid();
            outboxPublisher.AssertNotNull(nameof(outboxPublisher));
            processingOptions.AssertNotNull(nameof(processingOptions));

            await using var outboxTransaction = (SqlTransaction)(await sqlConnection.BeginTransactionAsync().ConfigureAwait(false));
            try
            {
                var results = await outboxTransaction
                              .ProcessPendingOutboxItemsAsync(
                    outboxPublisher : outboxPublisher,
                    processingOptions : processingOptions,
                    throwExceptionOnFailure : throwExceptionOnFailure
                    )
                              .ConfigureAwait(false);

                await outboxTransaction.CommitAsync().ConfigureAwait(false);

                return(results);
            }
            catch (Exception exc)
            {
                //FIRST Rollback any pending changes...
                await outboxTransaction.RollbackAsync().ConfigureAwait(false);

                try
                {
                    //THEN Attempt any Mitigating Actions for the Issue...
                    //IF WE have issues retrieving the new items from the DB then we attempt to increment the
                    //  Publish Attempts in case there is an issue with the entry that is causing failures, so
                    //  that any potential problematic items will eventually be failed out and skipped.
                    await sqlConnection.IncrementPublishAttemptsForAllPendingItemsAsync(outboxPublisher).ConfigureAwait(false);
                }
                catch (Exception mitigationExc)
                {
                    throw new AggregateException(new[] { exc, mitigationExc });
                }

                //FINALLY Re-throw to ensure we don't black hole the issue...
                throw;
            }
        }
        public static async Task Run([TimerTrigger("%TransactionalOutboxAgentCronSchedule%")] TimerInfo myTimer, ILogger log)
        {
            log.LogInformation($"Transactional Outbox Agent initiating process at: {DateTime.Now}");
            var configSettings = new SampleAppConfig();

            var azureServiceBusPublisher = new DefaultAzureServiceBusOutboxPublisher(
                configSettings.AzureServiceBusConnectionString,
                new AzureServiceBusPublishingOptions()
            {
                SenderApplicationName = $"{typeof(TransactionalOutboxAgentFunction).Assembly.GetName().Name}.{nameof(TransactionalOutboxAgentFunction)}",
                LogDebugCallback      = (s) => log.LogDebug(s),
                ErrorHandlerCallback  = (e) => log.LogError(e, "Unexpected Exception occurred while Processing the Transactional Outbox.")
            }
                );

            var outboxProcessingOptions = new OutboxProcessingOptions()
            {
                //ItemProcessingBatchSize = 200, //Only process the top X items to keep this function responsive!
                FifoEnforcedPublishingEnabled = true, //The Service Bus Topic is Session Enabled so we must processes it with FIFO Processing Enabled!
                LogDebugCallback      = (m) => log.LogDebug(m),
                ErrorHandlerCallback  = (e) => log.LogError(e, "Transactional Outbox Processing Exception"),
                MaxPublishingAttempts = configSettings.OutboxMaxPublishingRetryAttempts,
                TimeSpanToLive        = configSettings.OutboxMaxTimeToLiveTimeSpan
            };

            //************************************************************
            //*** Execute processing of the Transactional Outbox...
            //************************************************************
            await using var sqlConnection = new SqlConnection(configSettings.SqlConnectionString);
            await sqlConnection.OpenAsync().ConfigureAwait(false);

            await sqlConnection
            .ProcessPendingOutboxItemsAsync(azureServiceBusPublisher, outboxProcessingOptions)
            .ConfigureAwait(false);

            //************************************************************
            //*** Execute Cleanup of Historical Outbox Data...
            //************************************************************
            await sqlConnection
            .CleanupHistoricalOutboxItemsAsync(configSettings.OutboxHistoryToKeepTimeSpan)
            .ConfigureAwait(false);
        }
        public static async Task <ISqlTransactionalOutboxProcessingResults <Guid> > ProcessPendingOutboxItemsAsync(
            this SqlTransaction sqlTransaction,
            ISqlTransactionalOutboxPublisher <Guid> outboxPublisher,
            OutboxProcessingOptions processingOptions,
            bool throwExceptionOnFailure = false
            )
        {
            sqlTransaction.AssertSqlTransactionIsValid();
            outboxPublisher.AssertNotNull(nameof(outboxPublisher));
            processingOptions.AssertNotNull(nameof(processingOptions));

            //NOTE: Payload type isn't important when Publishing because we publish the already serialized
            //      payload anyway so to simplify the custom extension signature we can just use string payload type here.
            var outboxProcessor = new DefaultSqlServerTransactionalOutboxProcessor <string>(sqlTransaction, outboxPublisher);

            var results = await outboxProcessor
                          .ProcessPendingOutboxItemsAsync(processingOptions, throwExceptionOnFailure)
                          .ConfigureAwait(false);

            return(results);
        }
예제 #7
0
        public async Task <List <ISqlTransactionalOutboxItem <Guid> > > DoTestTransactionalOutboxCrawlingOfBlockingFailureItems(
            int testDataSize, int maxPublishingAttempts, bool enforceFifoProcessing, bool throwExceptionOnFailedItem)
        {
            //*****************************************************************************************
            //* STEP 1 - Prepare/Clear the Queue Table
            //*****************************************************************************************
            await MicrosoftDataSqlTestHelpers.PopulateTransactionalOutboxTestDataAsync(testDataSize);

            //*****************************************************************************************
            //* STEP 2 - Setup Custom Publisher & Processing Options...
            //*****************************************************************************************
            var publishedAttemptsList = new List <ISqlTransactionalOutboxItem <Guid> >();
            var failingPublisher      = new TestHarnessSqlTransactionalOutboxPublisher(
                (i, isFifoEnabled) =>
            {
                publishedAttemptsList.Add(i);
                TestContext.WriteLine($"Successful -- We have intentionally Failed to Publish Item: {i.UniqueIdentifier}");
                //Force an Error on Failure... this should result in ALL Publishing attempts to fail...
                throw new Exception("Failed to Publish!");
            }
                );

            var outboxProcessingOptions = new OutboxProcessingOptions()
            {
                MaxPublishingAttempts         = maxPublishingAttempts,
                FifoEnforcedPublishingEnabled = enforceFifoProcessing
            };

            //*****************************************************************************************
            //* STEP 3 - Executing processing of the Pending Items in the Queue...
            //*****************************************************************************************
            //Execute Processing of Items just inserted!
            //NOTE: We need to re-initialize a NEW Transaction and Processor to correctly simulate this running separately!
            ISqlTransactionalOutboxProcessingResults <Guid> publishedResults = null;
            int  loopCounter = 0;
            bool handledExceptionSoItsOkToContinue = false;

            do
            {
                await using var sqlConnection = await SqlConnectionHelper.CreateMicrosoftDataSqlConnectionAsync();

                await using var sqlTransaction = (SqlTransaction) await sqlConnection.BeginTransactionAsync().ConfigureAwait(false);

                handledExceptionSoItsOkToContinue = false;
                try
                {
                    publishedResults = await sqlTransaction.ProcessPendingOutboxItemsAsync(
                        failingPublisher,
                        outboxProcessingOptions,
                        throwExceptionOnFailedItem
                        ).ConfigureAwait(false);
                }
                catch (Exception exc)
                {
                    if (throwExceptionOnFailedItem)
                    {
                        //DO Nothing, as we Expect there to be exceptions when Throw Exception on Failure is Enabled!
                        TestContext.WriteLine($"Successfully handled expected Exception: {exc.Message}");
                        publishedResults = new OutboxProcessingResults <Guid>();
                        handledExceptionSoItsOkToContinue = true;
                    }
                    else
                    {
                        //IF we get an exception but ThrowExceptionOnFailure is disabled, then this is an issue!
                        throw;
                    }
                }

                await sqlTransaction.CommitAsync();

                //Provide Infinite Loop fail-safe...
                loopCounter++;
                Assert.IsTrue(loopCounter <= (testDataSize * maxPublishingAttempts * 2), $"Infinite Loop Breaker Tripped at [{loopCounter}]!");

                //Assert there are never any successfully published items...
                Assert.AreEqual(0, publishedResults.SuccessfullyPublishedItems.Count);
            } while (publishedResults.FailedItems.Count > 0 || (throwExceptionOnFailedItem && handledExceptionSoItsOkToContinue));

            //*****************************************************************************************
            //* STEP 4 - Retrieve and Validate Data in the Database is updated and all have failed out...
            //*****************************************************************************************
            //Assert All Items in the DB are Successful!
            await using var sqlConnection2 = await SqlConnectionHelper.CreateMicrosoftDataSqlConnectionAsync();

            await using var sqlTransaction2 = (SqlTransaction) await sqlConnection2.BeginTransactionAsync().ConfigureAwait(false);

            var outboxProcessor2 = new DefaultSqlServerTransactionalOutboxProcessor <string>(sqlTransaction2, failingPublisher);

            var successfulResultsFromDb = await outboxProcessor2.OutboxRepository
                                          .RetrieveOutboxItemsAsync(OutboxItemStatus.Pending)
                                          .ConfigureAwait(false);

            //Assert the results from the DB match those returned from the Processing method...
            Assert.AreEqual(successfulResultsFromDb.Count, 0);

            var failedResultsFromDb = await outboxProcessor2.OutboxRepository
                                      .RetrieveOutboxItemsAsync(OutboxItemStatus.FailedAttemptsExceeded)
                                      .ConfigureAwait(false);

            //Assert the results from the DB match those returned from the Processing method...
            Assert.AreEqual(failedResultsFromDb.Count * maxPublishingAttempts, publishedAttemptsList.Count);
            foreach (var dbItem in failedResultsFromDb)
            {
                Assert.AreEqual(OutboxItemStatus.FailedAttemptsExceeded, dbItem.Status);
                Assert.AreEqual(maxPublishingAttempts, dbItem.PublishAttempts);
            }

            //RETURN the Attempted Publishing list for additional validation based on the Pattern
            //  we expect for items to be processed.
            return(publishedAttemptsList);
        }