10
votes

I'm new to Azure Service Bus and I'm trying to establish a transactional strategy for queuing messages. Since SQL Azure doesn't support MSDTC and, therefore, with a distributed TransactionScope, I can't make use of it. So, I have a Unit of Work that can handle my database transactions manually.

The problem is that I can only find people using TransactionScope to handle both database and Azure Service Bus operations. Is there any other magical way to achieve transactions on Service Bus without using TransactionScope?

Thanks.

2
Have you considered Sagas?Peter Ritchie

2 Answers

6
votes

If you will be using cloud hosting or service like azure service bus, you should start considering to give up on two phase commits (2PC) or distributed transactions (DTC).

Instead, use a per-resource transactions (i.e. a transaction for a SQL command or a transaction for a Service Bus operation) carefully. And avoid transactions that cross that resource boundary.

You can then knit those resources/components operations together using reliable messaging and patterns like sagas, etc. for workflow management and error compensation. And scale out from there.

2PC in the cloud is hard for all sorts of reasons (but not impossible, you still can use IaaS).

2PC, as implemented by DTC, effectively depends on the coordinator and its log and connectivity to the coordinator to be very highly available. It also depends on all parties cooperating on a positive outcome in an expedient fashion. To that end, you need to run DTC in a failover cluster, because it’s the Achilles heel of the whole system and any transaction depends on DTC clearing it.

I'll quote a great example here of how to think of a 2PC transaction as a series of messages/actions and compensations

The grand canonical example for 2PC transactions is a bank account transfer. You debit one account and credit another.

These two operations need to succeed or fail together because otherwise you are either creating or destroying money (which is illegal, by the way). So that’s the example that’s very commonly used to illustrate 2PC transactions.

The catch is – that’s not how it really works, at all. Getting money from one bank account to another bank account is a fairly complicated affair that touches a ton of other accounts. More importantly, it’s not a synchronous fail-together/success-together scenario.

Instead, principles of accounting apply (surprise!). When a transfer is initiated, let’s say in online banking, the transfer is recorded in form of a message for submission into the accounting system and the debit is recorded in the account as a ‘pending’ transaction that affects the displayed balance.

From the user’s perspective, the transaction is ’done’, but factually nothing has happened, yet. Eventually, the accounting system will get the message and start performing the transfer, which often causes a cascade of operations, many of them yielding further messages, including booking into clearing accounts and notifying the other bank of the transfer.

The principle here is that all progress is forward. If an operation doesn’t work for some technical reason it can be retried once the technical reason is resolved.

If operation fails for a business reason, the operation can be aborted – but not by annihilating previous work, but by doing the inverse of previous work. If an account was credited, that credit is annulled with a debit of the same amount.

For some types of failed transactions, the ‘inverse’ operation may not be fully symmetric but may result in extra actions like imposing penalty fees.

In fact, in accounting, annihilating any work is illegal – ‘delete’ and ‘update’ are a great way to end up in prison.

0
votes

You can use TransactionScope. It works pretty well. Even though you have sent a message, but something other (database update) fails, then the message will be not published to the queue.

using var scope = new TransactionScope(TransactionScopeOption.RequiresNew, TransactionScopeAsyncFlowOption.Enabled);
try
{
        var messageBody = "This is my message";
        var message = new Message(Encoding.UTF8.GetBytes(messageBody));
        await _queueClient.SendAsync(message);

        await _myOtherService.FailPotentially();            // if this method fails then message will ne rolled back
        
        scope.Complete();
}
catch (Exception exception)
{
    scope.Dispose();
    _logger.LogError(" ... ", exception);
    throw;
}

Based on ServiceBus documentation you must use standard pricing instad of basic