Transaction scopes are a natural part of Windows Workflow Foundation; they provide transactional semantics to a set of activities bound together by their enclosing scope. The transaction scope provides ACID semantics to a group of workflow operations, ensuring that they abort as a group or succeed as a group, but not otherwise.
Out-of-the-box, however, the usability of workflow transaction scopes (at least for my recent application) was fairly low. When a transaction rolls back within a transaction scope, its owning workflow is brutally terminated by default, removing its state information from the workflow persistence service. Effectively, when a transaction aborts for some reason, the workflow goes away with it.
An alternative I discovered is the ability to provide a fault handler for the activity enclosing the transaction scope, and handle the TransactionAbortedException or any other exception that escapes the scope. However, after handling the exception, the workflow’s state is not magically restored to the previous persistence point – the workflow simply proceeds as if the transaction scope completed successfully, defeating the whole purpose of a transaction.
For me, this was the absolute opposite of what I wanted. I wanted a transaction scope to guard a set of operations which transition the workflow from one persistence point (savepoint) to another, so that the workflow state remains consistent after the transition. Either the transaction fails, restoring the state to the previous savepoint, or the transaction succeeds, moving the workflow forward to the next savepoint.
For example, the following workflow depicts my intent. The workflow begins by performing some initial work, and then stores a savepoint of its current state (by using an activity decorated with the [PersistOnClose] attribute). Then, the workflow prints a message and begins a transaction. If the transaction succeeds, the workflow is again persisted to a savepoint and goes on to do more work. If the transaction fails, the workflow should restore itself to the previously established savepoint and repeat the process.
The mechanics of transitioning the workflow back to the previous savepoint are fairly simple, and involve a call to the WorkflowInstance.Abort method followed by WorkflowInstance.Resume. As a result, the workflow’s data is removed from memory and it is restored to its latest durable (persisted) state, and then resumed. Unfortunately, it’s impossible to call the synchronous WorkflowInstance methods from within the workflow thread, so a creative solution was in place.
This is how my SavepointScope custom activity was born. This activity provides a scope for enclosing sensitive operations which are supposed to transition the workflow from one persistence point to another (it doesn’t have to be a transaction, but it often will be). The SavepointScope automatically restores the workflow to the most recent savepoint if an error occurs while performing the activities within the scope – this is accomplished by combining the HandleFault method of the SavepointScope itself with an external local service called ReturnToSavepointService which asynchronously aborts and resumes the workflow from the latest savepoint. To ensure that the scope does not complete its execution before the workflow is aborted and resumed, the HandleFault method returns ActivityExecutionStatus.Faulting, causing the workflow runtime to stall while it is being aborted by the local service.
To further refine the idea, the SavepointScope activity provides an event handler which can decide whether to restore the state or not, and a retry count property which indicates how many times the state should be restored before the workflow is allowed to terminate. (Note that a proper implementation would store the current retry count in persistent storage, to make sure it is shared with other workflow runtimes and that it survives a system restart. For expository purposes, an in-memory cache will suffice.)
The implementation of the SavepointScope activity and the test workflow demonstrated in this post can be downloaded from my SkyDrive. Please note that this is not production-quality code, and some bugs might be pending :-)