Search code examples
wcftransactionsmsmq

WCF, MSMQ and independent transactions on 2 queues


I have built a WCF service that processes an MSMQ, let's call the service QueueService. The Contract looks like this:

// Each call to the service will be dispatched separately, not grouped into sessions.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class QueueServiceContract : IQueueServiceContract
{
    [OperationBehavior(TransactionScopeRequired = true)]
    public void QueueItems(List<Item> items)     // really should be called 'HandleQueueItems
    {
        //  Early in the processing I do:
        Transaction qTransaction = Transaction.Current;
        ...
        // I then check if the destination database is available.
        if(DB.IsAvailable)
            ... process the data
        else
            qTransaction.Rollback;
        ...
}

IQueueServiceContract looks like this:

// The contract will be session-less.  Each post to the queue from the client will create a single message on the queue.
[ServiceContract(SessionMode = SessionMode.NotAllowed, Namespace = "MyWebService")]
public interface IQueueServiceContract
{
    [OperationContract(IsOneWay = true)]
    void QueueItems(List<Item> items);
}

Relevant parts of the App.config for the queue service look like this.

<services>
  <service name="QueueService.QueueServiceContract">
    <endpoint address="net.msmq://localhost/private/MyQueueServiceQueue" binding="netMsmqBinding" contract="QueueService.IQueueServiceContract">
      <identity>
        <dns value="localhost" />
      </identity>
    </endpoint>
...
  <netMsmqBinding>
    <binding exactlyOnce="true" maxRetryCycles="1000" receiveRetryCount="1"
      retryCycleDelay="00:10:00" timeToLive="7.00:00:00" useActiveDirectory="false">
    </binding>
  </netMsmqBinding>

This all works fine. When the DB is not available, the Rollback causes the queue entry to be put in the retry subqueue that I have configured to retry every 10 mins for 7 days. Everything about it works and has been in production for 6 months or so.

Now I am adding logging to the service. The QueueService is going to queue log entries into another queue we will call: LogQueue. The requirement is that whether the qTransaction is rolled back or not, a message should be sent to the LogQueue indicating the status of the request.

In the QueueService app.config I have added:

  <client>
  <endpoint address="net.msmq://localhost/private/MyLogQueue"
    binding="netMsmqBinding" bindingConfiguration="NetMsmqBinding_ILogContract"
    contract="LogServiceReference.ILogContract" name="NetMsmqBinding_ILogContract">
    <identity>
      <dns value="localhost" />
    </identity>
  </endpoint>
</client>
...
    <binding name="NetMsmqBinding_ILogContract" timeToLive="7.00:00:00">
      <security mode="None" />
    </binding>

In the LogService app.config, I have:

    <service name="LogService.LogContract">
    <endpoint address="net.msmq://localhost/private/MyLogQueue" binding="netMsmqBinding" contract="LogService.ILogContract">
      <identity>
        <dns value="localhost" />
      </identity>
    </endpoint>
... 
  <netMsmqBinding>
    <binding exactlyOnce="true" maxRetryCycles="1000" receiveRetryCount="1" retryCycleDelay="00:10:00" timeToLive="7.00:00:00"  useActiveDirectory="false">
    </binding>
  </netMsmqBinding>
...

Then, at the end of the QueueItems method I do the following:

LogContractClient proxy = new LogContractClient();
proxy.LogTransaction(myLoggingInformation);             // This queues myLoggingInformation to the LogQueue.

This all works fine too... until... a database is not available and the transaction is rolled back.

The Rollback will happen before the call to proxy.LogTransaction and I will get:

System.ServiceModel.CommunicationException: 'An error occurred while sending to the queue: The transaction specified cannot be enlisted. (-1072824232, 0xc00e0058).Ensure that MSMQ is installed and running. If you are sending to a local queue, ensure the queue exists with the required access mode and authorization.'

If I move the proxy.LogTransaction before the qTransaction.Rollback the log entry is never put in the LogQueue.

My working theory is that WCF considers the operations on the two queues: read from QueueService queue and write to LogQueue, as a single transaction. So, if I try to write to the LogQueue after the Rollback the transaction has already ended, but if I write to the LogQueue before calling Rollback the write to the queue is also rolled back.

Is there any way I can retain the ability to rollback the queueService transaction while not simultaneously rolling back the LogService transaction?


Solution

  • I think you can fix this by wrapping the call to the log queue client in a transaction scope with TransactionScopeOption.Suppress set. This will force the action to happen outside of the ambient transaction.

    Something like:

    using (var scope = new TransactionScope (TransactionScopeOption.Suppress))
    {
        // Call to enqueue log message
    }
    

    Your theory makes perfect sense. Because you're using transactional queues WCF is ensuring transactional consistency by enlisting everything inside the message handler in a DTC transaction. That obviously includes the enqueue of the logging message, and is expected, if unwanted, behaviour.