Compensation


As we know, a typical WF program will perform work (execute activities) episodically. The effects of the execution of these activities are not necessarily, but often, visible outside of the boundaries of the WF program. To take a few concrete examples, one activity in a WF program might send an email, another activity might create and write to a file on the local hard drive, and yet another activity might call a web service.

If a WF program instance successfully executes these activities, but then later (perhaps a second later, or perhaps six months later) has its execution go awry, it might be desirable to try to undo the effects of the successfully completed activities. This is no different conceptually than what you might do in a C# program when you catch an exception. The logic of a C# catch block can attempt to undo the effects of the partially completed TRy block with which it is associated. For example, if the code in your TRy block created a file but failed during the writing of data to that file (and consequently threw an exception), the catch block might delete the file. Complicating this problem is the fact that your cleanup logic probably needs to figure out just how far the work in the TRy block proceeded before the exception occurred.

You are free to take this approach in WF programs. We have already discussed the fault-handling model of WF, and though you know now that the WF execution model differs from that of the CLR because of the asynchronous nature of the WF scheduler, it's nevertheless true that the purpose of a WF FaultHandlerActivity is essentially the same as that of a C# catch block. When a FaultHandler-Activity is executed, it means that something unexpected has happened in the execution of the WF program instance, and it also means that you have a chance to sort out the potentially messy state in which your program instance now finds itself.

Another strategy is to employ transactions so that work performed under the auspices of a transaction can be rolled back if the transaction cannot commit. Just as they do in CLR programs, transactions do play an important role in WF programs (and we will discuss transactions in Chapter 6, "Transactions"), but they are not a panacea. It is simply not practical to encompass long-running work (that spans episodes) in a single transaction; locks cannot be held for the required duration. Further complicating the situation is the fact that not all (or perhaps none) of the work represented by activities is performed locallythink of our activity that calls a web service; organizations will never give direct control of their transactional resources to external entities. Standards have emerged to address the transactional aspects of (short-lived) remote work, but such capabilities are not yet commonplace infrastructure. Use of transactions is therefore complementary to (and not a replacement for) other aspects of WF programming such as fault handling.

The Compensating State

The WF programming model permits the association of compensation logic with an activity. In a nutshell, the way compensation logic is expressed is very much the same as cancellation logic. An activity can implement a Compensate method if it wishes to express compensation logic in code.

Additionally, a composite activity can be given a CompensationHandler-Activity, so that the developer of a WF program can provide custom compensation logic for that composite activity.

The rules that govern the execution of compensation logic are a bit more complicated than those governing cancellation, and will be demonstrated in the examples of this section. At the level of the activity automaton, things are pretty simple. A compensatable activity that is in the Closedstate with a result of Succeeded is allowed (but not required) to make a transition from the Closedstate to the Compensatingstate. From the Compensating state, a transition can be made either to the Faulting state, or back to the Closedstate.

The expected path is from the Compensating state back to the Closed state, which leaves the activity with an execution result of Compensated (thus precluding a second transition to the Compensating state). These transitions are shown in Figure 4.11.

Figure 4.11. The Compensating state of the activity automatonCompensatable Activities


Not all activities are compensatable. In some cases, it might not be possible to describe reasonable compensation logic for an activity. Think of the Wait activity, which just waits for a timer to fire. After the timer fires, and the Wait activity completes, there is no way to undo the fact that the timer fired. In other cases it might not be desirable to utilize compensation (for example, if one takes the approach of doing best-effort cleanup within fault handlers).

In order to keep the WF programming model simple, compensation is an opt-in model. An activity must implement the ICompensatableActivity interface in order to be considered compensatable.

The ICompensatableActivity type is shown in Listing 4.24.

Listing 4.24. ICompensatableActivity

 namespace system.Workflow.ComponentModel {   public interface ICompensatableActivity   {     ActivityExecutionStatus Compensate(       ActivityExecutionContext context);   } } 


ICompensatableActivity defines one method, Compensate. Like Activity.Execute and Activity.Cancel, this method takes an Activity-ExecutionContext as a parameter and returns ActivityExecutionStatus. Also like Execute and Cancel, Compensate is an execution handler whose dispatch is scheduled via the WF runtime.

Just as with cancellation and fault handling, an activity can complete its compensation logic within the Compensate method and return ActivityExecutionStatus.Closed. An activity may alternatively implement long-running compensation logic, in which case a value of ActivityExecutionStatus.Compensating needs to be returned from the Compensate method.

As a simple example, consider the SendEmail activity shown in Listing 4.25.

Listing 4.25. SendEmail Activity with Compensation Logic

 using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities {   public class SendMail : Activity, ICompensatableActivity   {     // To, From, Subject, and Body properties elided...     protected override ActivityExecutionStatus Execute(       ActivityExecutionContext context)     {       MailService mailService = context.GetService<MailService>();       mailService.Send(To, From, Subject, Body);       return ActivityExecutionStatus.Closed;     }     ActivityExecutionStatus ICompensatableActivity.Compensate(       ActivityExecutionContext context)     {       MailService mailService = context.GetService<MailService>();       mailService.Send(To, From, "IGNORE:" + Subject);       return ActivityExecutionStatus.Closed;     }   } } 


The SendEmail activity's Compensate method simply sends a second email indicating that the original email should be ignored. A different approach might be to define a Recall method on the MailService type, and invoke that method from Compensate. In this manner, the MailService can decide the most appropriate action. In some situations, the second email might be sent, but in other cases (perhaps the original email had not been sent because the machine on which the WF program is executing is offline, or the destination mail server supported a recall mechanism for delivered but unread email), use of a full-featured email server might produce a more desirable outcome.

It is important to understand that we are not rolling back the work done by SendEmail in a transactional sense. We are logically undoing, or more accurately compensating for, that previously completed work. In the time that elapses between the first email and the second (or the recall), there may well have been side effects due to the visibility of the data. To take a different example, an Insert activity that inserts a row into a database could well have a Compensate method that deletes that row from the database. But, in the seconds, hours, or weeks that elapse before the row is deleted, any number of applications might have read and acted upon (even updated or deleted) the data in that row. For these reasons, it is important to carefully consider the requirements of the scenarios that are targeted by the compensation logic that you write.

Compensation Handlers

Composite activities can also implement ICompensatableActivity and provide custom compensation logic. More typically, though, the Compensate method of a composite activity will look like the example shown in Listing 4.26.

Listing 4.26. CompensatableSequence Activity

 using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities {   public class CompensatableSequence : Sequence, ICompensatableActivity   {     ActivityExecutionStatus ICompensatableActivity.Compensate(       ActivityExecutionContext context)     {       return ActivityExecutionStatus.Closed;     }   } } 


Clearly there is no actual compensation logic here.

However, by implementing ICompensatableActivity, the Compensatable-Sequence activity allows the WF program developer to associate a CompensationHandlerActivity with any CompensatableSequence. When the CompensatableSequence transitions to the Closed state from the Compensating state, the WF runtime schedules the execution of the CompensationHandlerActivity.

CompensationHandlerActivity is similar in purpose to the cancellation handlers and fault handlers discussed earlier. A transition to the Closed state from the Compensating state made by the parent of a CompensationHandlerActivity does not actually occur until the CompensationHandlerActivity completes its execution.

Listing 4.27 shows how a CompensationHandlerActivity is used in the definition of a WF program. Two CompensatableSequence activities are executed simultaneously, and each one defines a compensation handler.

Listing 4.27. Modeling Compensation Logic Using CompensationHandlerActivity

 <Sequence x:Name="s1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow">   <Interleave x:Name="i1">     <CompensatableSequence x:Name="seq1">       ...       <wf:CompensationHandlerActivity x:Name="ch1">         <WriteLine x:Name="w1" Text="In compensation handler 1" />       </wf:CompensationHandlerActivity>     </CompensatableSequence>     <CompensatableSequence x:Name="seq2">       . . .       <wf:CompensationHandlerActivity x:Name="ch2">         <WriteLine x:Name="w2" Text="In compensation handler 2" />       </wf:CompensationHandlerActivity>     </CompensatableSequence>   </Interleave>   <ThrowTypedFault x:Name="throw1" FaultMessage="oops" FaultType="{x:Type System.InvalidOperationException}" /> </Sequence> 


The two CompensatableSequence activities execute, and then a fault occurs. Now it is time to run compensation logic.

Default Compensation

Listing 4.27 illustrates how compensation handlers are defined, but we have not explained how they will be invoked. If we examine Listing 4.27, we see that an exception will be thrown by the THRowTypedFault activity that follows the Interleave containing the two CompensatableSequence activities.

It is the default fault handling and compensation logic of Sequence and Interleave, inherited from CompositeActivity, which ensures that the compensation handlers for the two compensatable child activities are run. When the THRowTypedFault activity moves to the Closedstate (with a result of Faulted), the exception is propagated to its parent activity, the Sequence. The Sequence does not have a child FaultHandlersActivity, but even so, it will check to see if it contains any child activities that require compensation. Because this is indeed the case, the compensation handlers of the two CompensatableSequence activities will be scheduled. The order in which they are scheduled is the reverse order in which they completed their execution.

When executed, the WF program in Listing 4.27 will output one of the two following results, depending upon which CompensatableSequence finished first:

 In compensation handler 1 In compensation handler 2 


Or:

 In compensation handler 2 In compensation handler 1 


Only when the compensation handlers complete their execution will the exception propagate up from the Sequence activity. In this example, the Sequence is the root of the WF program, but if it were not, the exception would be propagated up the tree of activities one composite activity at a time. As the exception propagates up, compensation of any completed, compensatable activities (and cancellation of executing activities) occurs in an orderly (and sequential) fashion until an appropriate FaultHandlerActivity handles the exception or the WF program instance terminates.

Default compensation is also triggered for the child activities of a composite activity if the composite activity transitions from the Canceling state to the Closed state and does not have a CancellationHandlerActivity.

The program in Listing 4.28 illustrates default compensation of our SendEmail activity.

Listing 4.28. Default Compensation of a Primitive Activity

[View full width]

<Sequence xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <SendMail x:Name="send1" From="Bob" To="Dharma" Subject="Hello" /> <ThrowTypedFault FaultMessage="oops" FaultType="{x:Type System.InvalidOperationException }" /> </Sequence>


The Execute method of SendEmail sends out the email message. After an exception is thrown and propagated by the ThrowTypedFault activity, the Sequence moves to the Faulting state. The Sequence does not have a fault handler to handle the exception, but it will schedule the Compensate method of the SendMail and wait for its Closed event before propagating the exception. The compensation logic of SendEmail will send the second email (or the recall). In this scenario, the exception is unhandled by the root of the WF program and hence the WF program instance will terminate.

If a compensatable activity such as SendEmail is executed more than one time (say, as a child activity of a While) then each of the iterations is independently compensatable. Compensation fundamentally relies upon the ability, discussed earlier in this chapter, to save and then retrieve a completed execution context. The compensation of such an activity entails the compensation (in reverse order of completion) of all the successfully completed iterations (execution contexts).

In the program shown in Listing 4.29, the While activity iterates over a CompensatableSequence tHRee times. Subsequently, the THRowTypedFault activity throws a fault that is not handled by the program. Just before it is propagated by the Sequence, the compensation of each of the iterations of While takes place.

Listing 4.29. Compensation and AECs

 <Sequence x:Name="s1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow">   <While x:Name="while1" >     <While.Condition>       <ConstantLoopCondition MaxCount="3" />     </While.Condition>     <CompensatableSequence x:Name="body">       <GetCurrentTime x:Name="t1" />       <WriteLine x:Name="w1" Text="{wf:ActivityBind t1,Path=Time}" />       <Wait x:Name="wait1" Duration="00:00:04" />       <wf:CompensationHandlerActivity x:Name="ch1">         <WriteLine x:Name="w2" Text="{wf:ActivityBind t1,Path=Time}" />       </wf:CompensationHandlerActivity>     </CompensatableSequence>   </While>   <ThrowTypedFault x:Name="throw1" FaultMessage="oops" FaultType="{x:Type System.InvalidOperationException}" /> </Sequence> 


The WF program of Listing 4.29 produces the following output:

 6/10/2006 7:29:24 PM 6/10/2006 7:29:28 PM 6/10/2006 7:29:32 PM 6/10/2006 7:29:32 PM 6/10/2006 7:29:28 PM 6/10/2006 7:29:24 PM 


Let's analyze the program in Listing 4.29. The While activity is configured to iterate over a CompensatableSequence tHRee times. During each iteration, a GetCurrentTime activity (t1) is executed, followed by a WriteLine activity (w1), followed by a Wait activity (wait1). GetCurrentTime simply takes a snapshot of the current time in its Execute method (by calling DateTime.Now.ToString()) and sets its Time property to this value. The WriteLine activity's Text property is databound to the GetCurrentTime activity's Time property.

Within the compensation handler of the CompensatableSequence, the Text property of the WriteLine activity w2 is databound to the Text property of the WriteLine activity w1. Because the encompassing AEC of a compensation handler is restored during compensation, the application state that was saved at the time the AEC was persisted is available. Because the two WriteLine activities w1 and w2 reside within the same AEC, instances of w2 (the WriteLine inside the CompensationHandlerActivity) are able to print out values that were produced earlier (by instances of w1).

Because compensation of activities that implement ICompensatableActivity might not need to be triggered in a given program instance, compensatable activities that move to the Closed state with a result of Succeeded are not uninitialized until they are either compensated or until the WF program instance completes.

Custom Compensation

Default compensation unfolds naturally and implicitly as a part of fault handling and cancellation handling in a WF program instance. Of course, no compensation at all will ever happen if a WF program contains no compensatable activities (or, more precisely, if no compensatable activities in a WF program successfully complete their execution).

In some cases, the algorithm used by the WF runtime to implement default compensation does not correspond to the desired compensation logic. For these situations, WF includes an activity type named CompensateActivity that allows the WF program developer to exert a finer level of control over the compensation process.

The CompensateActivity type is shown in Listing 4.30.

Listing 4.30. CompensateActivity

 namespace System.Workflow.ComponentModel {   public sealed class CompensateActivity : Activity    {     public string TargetActivityName { get; set; }     /* *** other members *** */   } } 


The most common usage of CompensateActivity occurs when a WF program developer wishes to specify custom compensation logic in a Compensation-HandlerActivity and also trigger default compensation as part of that composite activity's compensation logic. To meet this requirement, the Compensate-Activity is added to a CompensationHandlerActivity and the value of its TargetActivityName property is set to the name of the associated composite activity. This configuration indicates that when the CompensateActivity executes, the default (implicit) compensation algorithm should run. This will schedule the execution of the compensation logic for all appropriate (successfully completed) compensatable activities in the composite activity's subtree.

Listing 4.31 defines explicit compensation for the CompensatableSequence that, when all is said and done, does exactly what implicit compensation (a CompensatableSequence with no CompensationHandler at all) would do, except that it also writes two messages to the console, one on either side of the default compensation logic that is triggered by the execution of the CompensateActivity.

Listing 4.31. Explicit Compensation Using CompensateActivity

 <CompensatableSequence x:Name="seq1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow">   ...   <wf:CompensationHandlerActivity x:Name="ch1">     <WriteLine x:Name="w1" Text="Time to run compensation" />     <wf:CompensateActivity x:Name="c1" TargetActivityName="seq1" />     <WriteLine x:Name="w2" Text="Done compensating" />   </wf:CompensationHandlerActivity> </CompensatableSequence> 


More advanced explicit compensation is also possible (for instance, in cases where it is not correct to initiate compensation sequentially in the reverse order of activity completion). For these cases, the CompensateActivity can be configured to reference a compensatable child activity of the associated composite activity. In this way, multiple CompensateActivity activities can be used to prescribe the order in which compensation unfolds. An example of such compensation logic is shown in Listing 4.32.

Listing 4.32. Explicit Compensation Using CompensateActivity

 <Sequence x:Name="seq1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow">   <CompensatableInterleave x:Name="i1">     <CompensatableSequence x:Name="cs1">       <wf:CompensationHandlerActivity x:Name="ch1">         <WriteLine Text="In compensator ch1" x:Name="w1"/>       </wf:CompensationHandlerActivity>     </CompensatableSequence>     <CompensatableSequence x:Name="cs2">       <wf:CompensationHandlerActivity x:Name="ch2">         <WriteLine Text="In compensator ch2" x:Name="w2"/>       </wf:CompensationHandlerActivity>     </CompensatableSequence>     <wf:CompensationHandlerActivity x:Name="ch4">       <wf:CompensateActivity TargetActivityName="cs2"/>       <wf:CompensateActivity TargetActivityName="cs1"/>     </wf:CompensationHandlerActivity>   </CompensatableInterleave>   <wf:ThrowActivity FaultType="{x:Type System.InvalidOperationException}" /> </Sequence> 


In contrast to the program in Listing 4.29, which used default compensation, the program in Listing 4.32 models compensation explicitly using CompensateActivity. In this way, the program in Listing 4.32 controls the order in which the compensation of child activities occurs. The preceding program will output the following:

 In compensator ch2 In compensator ch1 


The CompensatableInterleave activity used in Listing 4.32 is a simple modification of the Interleave activity: CompensatableInterleave needs to implement the ICompensatableActivity interface, just like the CompensatableSequence of Listing 4.26.

There are a couple of additional things to observe about CompensateActivity.

First, this activity can only appear (at any depth) within a fault handler, a cancellation handler, or a compensation handler. The validation logic of CompensateActivity also ensures that the TargetActivityName property must refer to a compensatable activity.

Second, when CompensateActivity executes and its TargetActivityName property references a child activity that does not have a result of Succeeded, the CompensateActivity does nothing. This logic covers both the case of a target activity that did not compete successfully, as well as the case of a target activity that has already been compensated.

In summary, the examples shown in this section have illustrated how activity compensation can be used as one strategy within the broader implementation of fault handling and cancellation handling for WF programs. It is important to remember that compensation does not stand alone, but is rather a mechanism that might help you, in certain cases, implement the fault handling and cancellation handling that your WF programs need. If compensation logic is not helpful in this endeavor, it may be safely ignored due to the opt-in model for compensation adopted by the WF programming model.




Essential Windows Workflow Foundation
Essential Windows Workflow Foundation
ISBN: 0321399838
EAN: 2147483647
Year: 2006
Pages: 97

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net