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. In addition, the service will never participate in the client’s
transaction, so in effect my four logical transactional modes (Client, Client/Service, Service, None) do not
apply with queued services. 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>