July 10, 2011, 1:33 a.m.
posted by thorn
When a TRansactionScopeActivity successfully completes its execution, the ambient transaction is committed. As mentioned in the previous section, the WF runtime does not allow the execution of activities outside of a transactionScopeActivity during the execution of that transactionScopeActivity. So, when a transactionScopeActivity completes, it is possible that other activities may be waiting in the wings, their execution handlers ready to be dispatched by the WF runtime.
Before moving on to the dispatch of these other execution handlers, though, the WF runtime persists the WF program instance. The WF program instance is not passivated here (which would include disposal of the activity objects representing the instance in memory), only persisted. This is a critical step because it means that the forward progress of the WF program instance is being indelibly recorded.
Furthermore, the persistence of the WF program instance takes place as part of the same transaction that was created for the transactionScopeActivity. Only with this design can the WF runtime guarantee that the recorded forward progress of the WF program instance remains in lock-step with the transactional commitment of the program's side effects (which are caused by the execution of activities, such as an UpdateCustomerInfo activity, within a transactionScopeActivity). As a consequence, a WF program containing a transactionScopeActivity can be executed only by a WF runtime that is configured with a persistence service.
Consider what would happen if this were not the case (i.e., if WF program instance persistence did not occur as part of the completion of a TRansaction-ScopeActivity). For our thought experiment, let's suppose that we use the UpdateCustomerInfo activity within a TRansactionScopeActivity like this:
<Sequence> ... <Interleave> <wf:TransactionScopeActivity> <UpdateCustomerInfo ... /> ... </wf:TransactionScopeActivity> <WriteLine Text="hello" /> ... </Interleave> </Sequence>
As part of the execution of the transactionScopeActivity, UpdateCustomerInfo successfully updates data in some database tables. When the transactionScopeActivity completes its execution (successfully), the transaction commits. Now, let's see what can happen if the WF runtime does not persist the WF program instance at this point in time. With the transactionScopeActivity completed, the WriteLine activity is ready to run (its Execute method had already been scheduled for execution by its parent Interleave activity, but could not be dispatched while execution of transactionScopeActivity was in progress).
Boom! The CLR application domain that is hosting the WF runtime now fails (maybe the machine crashes, or there is a power outage). It doesn't matter exactly how or when this failure occurs, so long as it happens before the next point in time at which the WF program instance is persisted. All is not lost, of course, because the WF runtime can be started again in a fresh CLR application domain, and WF program instances, including the one in our example, can have their execution resumed from their last persistence point.
But here we face the problem. The last persistence point for our example WF program instance occurred some arbitrary time before the transactionScopeActivity began its execution. When this WF program instance resumes after the WF runtime is started again, the transactionScopeActivity will execute again, and will in all likelihood fail when it tries to once again perform database operations. Remember, the database operations were previously committed in a transactional manner, but persistence of the instance was not part of this transaction. As we have shown, decoupling these actions is simply not viable.
Our thought experiment concluded, we now know why a WF program instance is always persisted by the WF runtime within the same transaction that is created for the execution of a transactionScopeActivity.
Custom Persistence Points
Completion of a transactionScopeActivity is not the only circumstance that leads to the persistence of a WF program instance. As we saw in Chapter 5, the application hosting the WF runtime can load and unload WF program instances as it pleases, using the methods available on the System.Workflow.Runtime.WorkflowInstance type. When a WF program instance is unloaded (passivated), it is persisted in addition to being disposed. A WF program instance is also persisted when its execution concludes.
Within a WF program, there are two ways to model persistence points. One is to use a transactionScopeActivity, as we have already discussed. The second way is to use custom persistence points. Custom persistence points occur at the completion of any activity whose type is decorated with (or inherits) PersistOnClose-Attribute, an attribute type that is defined in the System.Workflow. ComponentModel namespace.
PersistOnCloseAttribute can be applied to any activity type that you develop, but for the simplest case you can write an activity whose sole purpose is to force a persistence point, as shown in Listing 6.3.
Listing 6.3. SavePoint Activity that Models a Persistence Point
Persistence is not allowed to happen during execution of a transactionScope-Activity. Thus, the validator of transactionScopeActivity will flag as an error the presence of any activity whose type is decorated with (or inherits) [PersistOnClose].