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