Skip to content
Matt M edited this page Aug 2, 2019 · 14 revisions

Distributed transactions?

Out of the box, Rebus will NOT use DTC. This is a deliberate design choice, because it is our opinion that data consistency and resiliency against failures are much better handled by being conscious about how those things work and what the consequences are, instead of relying on a black box to handle it.

Another thing is that distributed transactions are usually quite expensive, performance-wise, because of the communication overhead involved. Also, since DTC (as everyone else) is subject to the CAP theorem, it will have to give up availability (the 'A' in CAP) when a network partition occurs.

So, how do transactions work with Rebus then?

To put it shortly: By committing them in the right order.

Scenarios

I am not in a transaction - I want to send a message

Nothing to worry about here - just await bus.Send(theMessage) and all is good.

I am not in a transaction - I want to either publish a message or send several messages

Since publishing a message with Rebus consists of sending the message directly to possibly many subscribers, these two operations are equivalent (unless you're using one of the multicast-capable transports, like e.g. RabbitMQ or Azure Service Bus - then an await bus.Publish(anEvent) translates into one single publish).

So, if it's important to you that either ALL or NONE of the recipients/subscribers get the message, you must perform the operation in a transaction.

Rebus has a built-in transaction scope mechanism called RebusTransactionScope, which is used like this:

using(var scope = new RebusTransactionScope())
{
    await bus.Send(firstMessage);
    await bus.Send(secondMessage);

    // commit it like this
    await scope.CompleteAsync();
}

which is the preferred way of doing this, because it has an async way of being completed (which is where messages actually get sent, when using transports that do not actually support transactions).

What if I want .NET's transaction scope?

You can also let Rebus enlist in an ordinary ambient .NET transaction like so (via the Rebus.TransactionScopes package):

using(var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
    tx.EnlistRebus(); //< remember to hook Rebus' scope up with .NET's scope

    // perform several calls to the queueing system in here
    await bus.Send(something);
    await bus.Send(somethingElse);
    await bus.Publish(whatever);

    tx.Complete();
}

It should be noted that not all transports support transactions, so – depending on your choice of transport – the shown operation might turn into several calls to the transport in the end.

I am in a Rebus message handler

In this special case, Rebus will create a queue transaction which will surround the entire processing of the message. This means that Rebus will

  • receive the incoming message
  • send/publish all outgoing messages

as a part of the same queue transaction(*).

This also means that if your work fails, the queue transaction is rolled back, and nothing is lost.

Again, this is of course only possible if the queueing system supports transactions in the first place. If not, transactional behavior is emulated by deferring the actual sending of the outgoing messages until after your code has finished executing.

THE ONLY THING TO LOOK OUT FOR: is if your work SUCCEEDS, but committing the queue transaction fails, then the incoming message will be received again. But this time, you will already have performed your work - but none of your outgoing messages will have been sent.

In this case - and you may already have figured this out - you must keep adhering to the rules of how to implement idempotency, which state that you must perform the same externally visible actions, even when your internal state reveals that you have already processed the incoming message.

This way, a strict "at least once-delivery guarantee" is kept all the way through, and can be guaranteed at all times, no matter how you might decide to let your chaos monkey kill machines and/or processes.

With MSMQ this is highly unlikely, though, because the queueing system is residing on each machine, and thus there's no remote communication involved. It's a thing to look out for with RabbitMQ and other transports that rely on making remote connections though.

I want to use DTC!

If you want to use DTC, you'll have to do some work yourself. At the moment, no transport implementation exists for Rebus that can enlist in a distributed transaction.

It shouldn't be that hard to create one though, e.g. by grabbing the existing MsmqTransport, copy the source code to a new class, e.g. DtcMsmqTransport, and then use MSMQ in a way that enlists the queue transaction in a way that allows for DTC escalation.


(*) It should be noted that the transactional behavior in message handlers does NOT include send/publish operations carried out on other bus instances. Each bus instance will exhibit transactional behavior with regards to itself, but if you receive a message with one bus instance and in its message handler you use ANOTHER bus instance to send/publish a message, that message will be sent/published immediately.

Clone this wiki locally