MSMQ is a WCF transactional resource manager. When you
create a queue (either programmatically or administratively), you can
create the queue as a transactional queue. If the queue is
transactional, it is durable, and messages always persist to disk. More
importantly, posting messages to and removing messages from the queue
will always be done under a transaction. If the code that tries to
interact with the queue has an ambient transaction, the queue will
silently join that transaction. If no ambient transaction is present,
MSMQ will start a new transaction for that interaction. It is as if the queue is encased
in a TransactionScope constructed
with TransactionScopeOption.Required. Once in a transaction, the
queue will commit or roll back along with the accessing transaction. For
example, if the accessing transaction posts a message to the queue and
then aborts, the queue will reject the message.
1. Delivery and Playback
When a nontransactional client calls a queued service,
client-side failures after the call will not roll back posting the
message to the queue, and the queued call will be dispatched to the
service. However, a client calling a queued service may call under a
transaction, as shown in Figure 1.
The client calls are converted to WCF messages and then packaged
in an MSMQ message (or messages). If the client’s transaction commits,
these MSMQ messages are posted
to the queue and persist there. If the client’s transaction aborts,
the queue discards these MSMQ messages. In effect, WCF provides
clients of a queued service with an auto-cancellation mechanism for
their asynchronous, potentially disconnected calls. Normal connected
asynchronous calls cannot be combined easily, if at all, with
transactions, because once the call is dispatched there is no way to
recall it in case the original transaction aborts. Unlike connected
asynchronous calls, queued service calls are designed for this very
transactional scenario. In addition, the client may interact with
multiple queued services in the same transaction. Aborting the client
transaction for whatever reason will automatically cancel all calls to
those queued services.
1.1. The delivery transaction
Since the client may not be on the same machine as the
service, and since the client, the service, or both could be
disconnected, MSMQ maintains a client-side queue as well. The
client-side queue serves as a “proxy” to the service-side queue. In
the case of a remote queued call, the client first posts the message
to the client-side queue. When (or if) the client is connected, MSMQ
will deliver the queued messages from the client-side queue to the
service-side queue, as shown in Figure 2.
Since MSMQ is a resource manager, removing the message from
the client-side queue will create a transaction (if indeed the queue
is transactional). If MSMQ fails to deliver the message to the
service-side queue for whatever reason (such as a network fault or
service machine crash), the delivery transaction will abort, the
message removal from the client-side queue will be rolled back, and
the message posting to the service-side queue will also be canceled,
resulting in the message being back in the client-side queue. At
this point, MSMQ will try again to deliver the message. Thus, while
you can configure and control
failure handling (as you will see later), excluding fatal errors
that can never be resolved, queued services actually enjoy a
guaranteed delivery mechanism; if it is technically possible to
deliver the message (within the confines of the failure-handling modes), the message will get
from the client to the service. In effect, this is WCF’s way of
providing reliable messaging for queued services. Of course, there
is no direct support for the reliable messaging protocol, as there
is with connected calls; this is just the analogous
mechanism.
1.2. The playback transaction
When WCF removes a message from the queue for playback to the
service, this kick-starts a new transaction (assuming the queue is
transactional), as shown in Figure 3.
The service is usually configured to participate in the
playback transaction. If the playback transaction aborts (usually
due to service-side exceptions), the message rolls back to the
queue, where WCF detects it and dispatches it again to the service.
This, in effect, yields an auto-retry mechanism. Consequently, you
should keep the service’s processing of the queued call relatively
short, or risk aborting the playback transaction. An important
observation here is that it is wrong to equate queued calls with
lengthy asynchronous calls.
2. Service Transaction Configuration
As just demonstrated, assuming transactional queues, there are
actually three transactions involved in every queued call: client,
delivery, and playback, as shown in Figure 4.
From a design perspective, you rarely, if ever, depict the
delivery transaction in your design diagrams and you simply take it
for granted.Configuring the service
contract operation with TransactionFlowOption.Allowed or TransactionFlowOption.NotAllowed leads to
the same result—the client transaction is never provided to the
service. Not only that, but TransactionFlowOption.Mandatory is
disallowed for configuration on a queued contract, and this constraint
is verified at the service load time. The real issue is the relation
between the playback transaction and the service transactional
configuration.
2.1. Participating in the playback transaction
From a WCF perspective, the playback transaction is treated as
the incoming transaction to the service. To participate in the
playback transaction, the service needs to have the operation
behavior configured with TransactionScopeRequired set to true, as shown in Example 1 and graphically in
Figure 3.
Example 1. Participating in the playback transaction
[ServiceContract] interface IMyContract { [OperationContract(IsOneWay = true)] void MyMethod(); } class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod() { Transaction transaction = Transaction.Current; Debug.Assert(transaction.TransactionInformation. DistributedIdentifier != Guid.Empty); } }
|
An interesting point made in Example 1 is that with both
MSMQ 3.0 and MSMQ 4.0, every transaction always uses the DTC for
transaction management, even in the case of a single service and a
single playback. This might change in the next release of WCF and
the .NET Framework.
2.2. Ignoring the playback transaction
If the service is configured for not having any transactions
(like the service shown in Example 2), WCF will still
use a transaction to read the message from the queue, except that
transaction will always commit (barring an unforeseen failure in
MSMQ itself). Exceptions and failures at the service itself will not
abort the playback transaction.
Example 2. Ignoring the playback transaction
[ServiceContract] interface IMyContract { [OperationContract(IsOneWay = true)] void MyMethod(); } class MyService : IMyContract { public void MyMethod() { Transaction transaction = Transaction.Current; Debug.Assert(transaction == null); } }
|
This scenario is depicted graphically in Figure 5.
Services that do not participate in the playback transaction
will not have the benefit of automated retries by WCF in the case of
a playback failure, and it is possible for the played-back call to
fail while the de-queued transaction commits. The main motivation
for configuring queued services this way is to accommodate lengthy
processing. If the service does not participate in the playback
transaction, the call can take any amount of time to
complete.
2.3. Using a separate transaction
You can also write a service so that it manually requires a
new transaction, as shown in Example 3.
Example 3. Using a new transaction
class MyService : IMyContract { public void MyMethod() { using(TransactionScope scope = new TransactionScope()) { ... scope.Complete(); } } }
|
This scenario is depicted in Figure 6.
When the service uses its own new transaction for each
message, it should also prevent
participating in the playback transaction (by defaulting to
the TransactionScopeRequired value of false) so as not to affect the playback
transaction in any way. Again, this negates the benefit of the
auto-retry mechanism. However, having a new transaction separate
from the playback transaction gives the service the opportunity to
perform its own transactional work. You would typically configure a
service to use its own transaction when the queued operation being
called is nice to have and should be performed under the protection
of a transaction, yet does not need to be retried in case of a
failure.
3. Nontransactional Queues
The MSMQ queues described so far were both durable and
transactional. The messages persisted to the disk, and posting a
message to and reading it from the queue was transactional. However,
MSMQ also supports nontransactional queues. Such queues can be durable
and persist on the disk or can be volatile (stored in memory). If the
queue is volatile, the messages in the queue will not persist across a
machine shutdown or a machine crash or just recycling of the MSMQ
service.
When you create a queue (either using the MSMQ administration
tool or programmatically), you can configure it to be transactional or
not, and that selection is fixed for the life of the queue.
Nontransactional queues do not offer any of the benefits of
transactional messaging systems, such as auto-cancellation, guaranteed
delivery, and auto-retries. When using a nontransactional queue, if
the client transaction aborts, the message or messages will stay in
the queue and be delivered to the service. If the playback transaction
aborts, the messages will be lost.
As inadvisable as it is, WCF can work with nontransactional
queues. MsmqBindingBase (the
base class of NetMsmqBinding)
offers the two Boolean properties Durable and ExactlyOnce, and these properties default to
true:
public abstract class MsmqBindingBase : Binding,...
{
public bool Durable
{get;set;}
public bool ExactlyOnce
{get;set;}
//More members
}
public class NetMsmqBinding : MsmqBindingBase
{...}
To work with a nontransactional queue, the ExactlyOnce property must be set to false. This will enable you to work both
with volatile and durable queues. However, because of the lack of
guaranteed delivery, when using a volatile queue WCF requires that you
set the ExactlyOnce property of the
binding to false; otherwise, WCF
will throw an InvalidOperationException at the service
load time. Consequently, here is a consistent configuration for a
volatile nontransactional queue:
<netMsmqBinding>
<binding name = "VolatileQueue"
durable = "false"
exactlyOnce = "false"
/>
</netMsmqBinding>