Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Transactional Workflows: Suspend-Enqueue-Unload-Resume Done Correctly on Second-Phase Commit

DZone's Guide to

Transactional Workflows: Suspend-Enqueue-Unload-Resume Done Correctly on Second-Phase Commit

·
Free Resource

More than two years ago I visited the subject of transactionally delivering a message to a workflow, making sure that the transaction did not commit until the message has been delivered and the workflow persisted under the same transaction. This subject has also been covered fairly well in this MSDN Forums thread.

As a quick reminder, if you want to transactionally deliver a message to a workflow, you need to follow these steps:

  • Suspend the workflow instance
  • Enqueue the message to the workflow’s queue
  • Unload the workflow, thus persisting it within the context of the ambient transaction
  • Resume the workflow

This last step must not execute under the same transaction, at least when using WF 3.5 and the default SQL persistence service, or else you would encounter an SQL timeout exception in the stored procedure that attempts to update the workflow state when resuming it. This code will not work:

WorkflowInstance instance = …;
using (TransactionScope tx = new TransactionScope())

{

instance.Suspend("Suspending to enqueue work");

instance.EnqueueItem("MyQueue", "Hello World", null, null);

instance.Unload();

instance.Resume();

tx.Complete();

}

If you run this sample with a default SQL persistence service, you’ll receive an exception:

Unhandled Exception: System.Data.SqlClient.SqlException: Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.
at System.Data.SqlClient.SqlConnection.OnError
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning
at System.Data.SqlClient.TdsParser.Run
at System.Data.SqlClient.SqlDataReader.ConsumeMetaData
at System.Data.SqlClient.SqlDataReader.get_MetaData
at System.Data.SqlClient.SqlCommand.FinishExecuteReader
at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds
at System.Data.SqlClient.SqlCommand.RunExecuteReader
at System.Data.SqlClient.SqlCommand.RunExecuteReader
at System.Data.SqlClient.SqlCommand.ExecuteReader
at System.Data.SqlClient.SqlCommand.ExecuteDbDataReader
at System.Workflow.Runtime.Hosting.PersistenceDBAccessor.RetrieveStateFromDB
at System.Workflow.Runtime.Hosting.PersistenceDBAccessor.RetrieveInstanceState
at System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService.LoadWorkflowInstanceState
at System.Workflow.Runtime.WorkflowRuntime.InitializeExecutor
at System.Workflow.Runtime.WorkflowRuntime.Load
at System.Workflow.Runtime.WorkflowInstance.Resume

To address this, you might be tempted to write something along the following lines:


using (TransactionScope tx = new TransactionScope())

{

instance.Suspend("Suspending to enqueue work");

instance.EnqueueItem("MyQueue", "Hello World", null, null);

instance.Unload();

using (new TransactionScope(TransactionScopeOption.Suppress))

{

instance.Resume();

}

tx.Complete();

}

However, this is just another way of saying that you want the ambient transaction to wait until the Resume operation returns, but the Resume operation requires the locks acquired by the Suspend operation—in other words, you just created a deadlock, and you’ll get exactly the same exception.

The only appropriate way to resume the workflow is after the transaction completes. To be precise, if the transaction aborts, there’s no reason for you to resume the workflow because the Suspend operation would be rolled back as well (you can easily test this). If the transaction commits, however, you want to resume the workflow so that it can process the work item. Here’s how:


using (TransactionScope tx = new TransactionScope())

{

instance.Suspend("Suspending to enqueue work");

instance.EnqueueItem("MyQueue", "Hello World", null, null);

instance.Unload();

Transaction.Current.TransactionCompleted +=

(o, e) =>

{

if (e.Transaction.TransactionInformation.Status == TransactionStatus.Committed)

{

instance.Resume();

}

};

tx.Complete();

}

This works, and achieves the desired effect—if the transaction commits, the workflow is resumed. There is, unfortunately, a window of opportunity for failure between the time that the transaction commits and the time the Resume operation executes. If during the Resume operation there is a system error, the transaction would be deemed complete but the workflow would remain in a suspended state. Fortunately, this situation can be addressed by resuming workflows automatically (e.g. using filtering by suspension reason) when the system restarts.

Topics:

Published at DZone with permission of Sasha Goldshtein, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}