See the Intro to this series, which has links to all the parts.
Depending on the business event represented by a message type, a message needs to be either transactional or non-transactional.
A transactional message has higher guarantees about delivery — MSMQ guarantees that a transactional message will be delivered exactly once to the destination. No such guarantee applies to non-transactional messages. In addition, WCF’s NetMsmqBinding has many options that apply only to transactional messages.
MSMQ is finicky about transactions. When a queue is created, you must specify whether it is transactional or non-transactional, and that setting cannot be changed. (I’m going to use “tx” instead of “transactional” from here on out.)
Futhermore, code that reads from or writes to a tx queue must be executing in a transaction, or the operation will fail. Code that reads from or writes to a non-tx queue must *not* be executing in a transaction, or the operation will fail.
The nice thing about handling messages in a transaction is that any resulting message publishes or sends, along with any database writes, are rolled back if the transaction rolls back.
For example, a common pattern is for a message handler to write to a database, then publish a notification message that “X was created or changed”. Handling the message transactionally guarantees that the outbound message will only be published if the database write actually succeeds. And that the database write will be rolled back if publishing the message fails for some reason (although that is extremely rare).
The not so nice thing about handling messages in a transaction is that it is 10-100 times slower than handling the same messages outside of a transaction.
So it’s important for us to limit the use of transactions to the message types that really need it.
As you might guess, if a handler is executing in a transaction, and it makes a database call, the DTC is invoked. That’s because MSMQ and Sql Server are both transaction managers. The DTC coordinates the transaction across the two.
Distributed transactions are a no-no in a scalable system. But the true evil comes from transactions that span logical service boundaries. The distributed transactions we allow are “local” in the sense that they always occur inside a single logical business component.
In Part 2, you may have noticed the ReceiveInTransaction() method on the IBusReceiver service interface. How does that fit in? Well, the implementation of IBus knows what type of message it is publishing or sending. It uses that to tack on a suffix to the Uri where the message should be delivered: to a Tx queue or a NonTx queue.
When the IBusReceiver WCF service starts, it creates a non-tx endpoint for the NonTx queue, and a tx endpoint for the Tx queue. That way, the single WCF service can handle both tx and non-tx messages.
Once the message gets into our code on the receiving side, we double check whether the message type is transactional. If it was received on the wrong queue, we throw an exception. When a developer specifies that a message type is tx, we can’t allow it to be processed non-transactionally, and vice versa. Doing so would change the system behavior in the event of a failure. Not to mention the reliability and performance characteristics for that message type.
In Part 4, I’ll talk about message types in more detail.