Skip to content
Mogens Heller Grabe edited this page Aug 16, 2016 · 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 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. You can do that by letting Rebus enlist in an ordinary ambient transaction like so (via the Rebus.TransactionScope package):

using(var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
    tx.EnlistRebus();

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

    tx.Complete();
}

This will work, because Rebus' transports are nice enough to enlist a lightweight queue transaction in the ambient transaction, thus allowing all queue interactions to be performed within that transaction, and thus committed/rolled back atomically.

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 remove 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.

Clone this wiki locally