Cancellation


In real-world processes, it is common to begin two or more activities simultaneously, but then demand that only a subset of the activities need to complete in order to reach the desired goal. For example, when you are trying to sell your car, you might simultaneously buy a classified advertisement in the local newspaper, place a listing at the top Internet used car marketplaces, and put a sign in the window of your car. If a neighbor sees the sign and offers to buy the car, the other avenues used to locate buyers are no longer needed and should be abandoned.

In C# programs, it is certainly possible to create multiple threads in order to perform work concurrently. The basic capabilities of threads, though, do not directly provide patterns for orderly cancellation of work; custom solutions must be devised. After all, we don't want to pay for newspaper and Internet advertisements after the car is sold. Furthermore, if the actual work to be done is performed outside of the C# programperhaps by remote web services or by an application that waits for input from people (a car can take weeks to sell)then the use of threads to manage this work is not workable anyway (as we discussed in Chapter 1, "Deconstructing WF").

We will now see how easy it is to write a WF composite activity that implements exactly the semantic required in the car sale scenario. Once you are on the road to developing these kinds of composite activities, you will be able to more precisely translate the requirements of real-world processes to the control flow of the WF programs you write.

The AnyOneWillDo composite activity used in the program of Listing 4.10 is very similar to Interleave because AnyOneWillDo will schedule all of its child activities for execution in a single burst during the AnyOneWillDo.Execute method. AnyOneWillDo differs from Interleave in that after a single child activity reports its completion (moves to the Closed state from the Executing state), the AnyOneWillDo activity is logically ready to report its own completion.

Listing 4.10. XAML that Uses an AnyOneWillDo Activity

 <AnyOneWillDo... >   <SellCarWithNewspaperAd />   <SellCarOnline />   <SellCarWithSignInWindow /> </AnyOneWillDo> 


There is a catch, though. The WF runtime will not allow any composite activity to move to the Closed state while child activities within that composite activity are still in the Executing state. A composite activity cannot logically be done with its work if its child activities are still executing (and are therefore not finished with their work).

A composite activity can only transition to the Closed state if every one of its child activities is either in the Initialized state (which means it was never asked to do its work) or in the Closed state. Because this enforcement applies recursively to those child activities that themselves are composite activities, it is assured that the entire subtree of a composite activity is quiet (that is, Initialized or Closed) before the composite activity itself moves to the Closed state.

To get around this, AnyOneWillDo could wait for the completion of all child activities, but that would defeat the purpose and take us back to exactly what Interleave does. Instead, AnyOneWillDo can request cancellation of the executing child activities after one of the child activities completes.

The Canceling State

The cancellation of a child activity is requested via the ActivityExecution-Context in exactly the same way as the request to schedule the execution of a child activity. Cancellation is a scheduled dispatch to the Cancel method of the child activity.

Listing 4.11 shows the CancelActivity method of AEC, along with ExecuteActivity, which we have used already.

Listing 4.11. ActivityExecutionContext.CancelActivity

 namespace System.Workflow.ComponentModel {   public sealed class ActivityExecutionContext : ...   {     public void CancelActivity(Activity activity);     public void ExecuteActivity(Activity activity);     /* *** other members *** */   } } 


We will return in a moment to what the child activities might implement in their Cancel method; first let's look at the code in AnyOneWillDo that achieves the cancellation.

Listing 4.12 shows a modified version of the ContinueAt method that we first developed for Interleave. When AnyOneWillDo receives notification that a child activity has moved to the Closed state, it calls the CompletionThresholdReached method.

CompletionThresholdReached is assumed to be a private helper method that iterates through the child activities, checking their ExecutionStatus and ExecutionResult properties to see if the necessary condition for completion has been met. In the case of AnyOneWillDo, if one child activity has successfully completed its execution, then all other child activities that are not yet in the Closed state (the logic of the Execute method, which is the same as that of Interleave, ensures that no child activity is still in the Initialized state) are scheduled for cancellation.

Listing 4.12. AnyOneWillDo.ContinueAt

 public class AnyOneWillDo : CompositeActivity {   ...   void ContinueAt(object sender,     ActivityExecutionStatusChangedEventArgs e)   {     e.Activity.Closed -= this.ContinueAt;     ActivityExecutionContext context =       sender as ActivityExecutionContext;     if (CompletionThresholdReached())     {       bool okToClose = true;       foreach (Activity child in this.EnabledActivities)       {         ActivityExecutionStatus status = child.ExecutionStatus;         if (status == ActivityExecutionStatus.Executing)         {           okToClose = false;           context.CancelActivity(child);         }         else if (status != ActivityExecutionStatus.Closed)         {           okToClose = false;         }       }       if (okToClose)         context.CloseActivity();     }   } } 


The call to CancelActivity immediately moves the child activity into the Canceling state from the Executing state. Just as with normal execution, it will be up to the child activity to decide when to transition to Closed from Canceling. When that transition does happen, the AnyOneWillDo activity will be notified of this change (it has already subscribed for the Closed event of the child activity, when it was scheduled for execution). Thus, only when all child activities are Closed, either by virtue of having completed their execution or by having been canceled, will the AnyOneWillDo activity report its own completion and move to the Closed state. At this point, only one of the child activities will have completed. The car has been sold, and the other work items that had started simultaneously have been canceled.

The AnyOneWillDo example illustrates how easily cancellation becomes a normal part of WF program execution. Cancellation is a natural and necessary characteristic of many real-world processes. In general, activities that begin their execution cannot assume that they will be allowed to complete their work (unless the work can be finished entirely within the Execute method).

In other words, real-world control flow has a natural notion of early completion. This stands in stark contrast to C# control flow. In composite activities that embody elements of real-world processes, the execution of some child activities must be canceled when the overall goal of the set of child activities has been met.

Let's now consider what happens to an activity that has been scheduled for cancellation. The relevant method of Activity is the Cancel method, which has the same signature as Execute:

 namespace System.Workflow.ComponentModel {   public class Activity : DependencyObject   {     protected internal virtual ActivityExecutionStatus Cancel(       ActivityExecutionContext context);     /* *** other members *** */   } } 


As we learned in the previous section, the Cancel method is the execution handler that is scheduled when a composite activity calls the CancelActivity method of AEC, passing the child activity to be canceled as the parameter.

The default implementation of the Cancel method, defined by the Activity class, is to immediately return a value of ActivityExecutionStatus.Closed, indicating that cancellation is complete. Activities needing to perform custom cancellation logic can override this method and add whatever cancellation logic is required.

Figure 4.7 depicts the Canceling state within the activity automaton.

Figure 4.7. The Canceling state in the activity automaton


Just as with execution, activity cancellation logic might require interaction with external entities; an activity may therefore remain in the Canceling state indefinitely before reporting the completion of its cancellation logic and moving to the Closed state. This is exactly the same pattern as normal activity execution.

The Wait activity that we developed in Chapter 3, "Activity Execution," is a simple example of an activity that should perform custom cancellation work. If a Wait activity is canceled, it needs to cancel its timer because the timer is no longer needed. The Wait activity can also delete the WF program queue that is used to receive notification from the timer service. The cancellation logic of the Wait activity is shown in Listing 4.13.

Listing 4.13. Wait Activity's Cancellation Logic

 using System; using System.Workflow.ComponentModel; using System.Workflow.Runtime; namespace EssentialWF.Activities {   public class Wait : Activity   {     private Guid timerId;     // other members per Listing 3.11     ...     protected override ActivityExecutionStatus Cancel(       ActivityExecutionContext context)     {       WorkflowQueuingService qService =         context.GetService<WorkflowQueuingService>();       if (qService.Exists(timerId))       {         TimerService timerService =           context.GetService<TimerService>();         timerService.CancelTimer(timerId);         qService.DeleteWorkflowQueue(timerId);       }       return ActivityExecutionStatus.Closed;     }   } } 


The Cancel method of the Wait activity does two things: It cancels the timer that was previously established using the TimerService, and it deletes the WF program queue that served as the location for the TimerService to deliver a notification when the timer elapsed.

When a canceled activity's transition to Closed occurs, its ExecutionResult property will from that point forward have a value of Canceled.

There is one subtle aspect to this cancellation logic, which is represented by the fact that the aforementioned cleanup is wrapped in a conditional check to see whether the WF program queue actually exists. The WF program queue will only exist if the Execute method of the Wait activity was executed. It would seem that this is an assertion we can make. But in fact it is not so. It is possible, though certainly not typical, for a Wait activity to move to the Executing state (when its parent schedules it for execution), and then to the Canceling state (when its parent schedules it for cancellation) without its Execute method ever being dispatched.

If the scheduler dequeues from its work queue an item representing the invocation of an activity's Execute method, but that activity has already moved to the Canceling state, then the work item corresponding to the Execute method is discarded. Invocation of the Execute execution handler would violate the activity automaton (because the activity is now in the Canceling state), and the reasonable expectation of the parent composite activity that had scheduled the cancellation of the activity. In short, the WF runtime enforces the activity automaton when scheduler work items are enqueued (for example, an activity cannot be canceled if it is already in the Closed state), and also when they are dequeued for dispatch.

In the case of the Wait activity, if the WF program queue does not exist, the Execute method was never actually invoked. If the Execute method was not invoked, no timer was established and hence there is no need to call the CancelTimer method of the TimerService.

We can illustrate the subtlety just described with a somewhat pathological composite activity, which schedules a child activity for execution and then requests its cancellation in consecutive lines of code. This activity's Execute method is shown in Listing 4.14.

Listing 4.14. Cancellation Overtakes Execution

 using System; using System.Workflow.ComponentModel; public class ChangedMyMind : CompositeActivity {   protected override ActivityExecutionStatus Execute(     ActivityExecutionContext context)   {     Activity child = this.EnabledActivities[0];     child.Closed += this.ContinueAt;     PrintStatus(child);     context.ExecuteActivity(child);     PrintStatus(child);     context.CancelActivity(child);     PrintStatus(child);     return ActivityExecutionStatus.Executing;   }   void ContinueAt(object sender,     ActivityExecutionStatusChangedEventArgs e)   {     PrintStatus(e.Activity);   }   void PrintStatus(Activity a)   {     Console.WriteLine(a.Name + " is " + a.ExecutionStatus +       " : " + a.ExecutionResult);   } } 


Though an extreme case, the ChangedMyMind activity suffices to illustrate the situation we have described. A request to schedule invocation of an activity's Cancel method is always made after a request to schedule invocation of that activity's Execute method; however, the dispatcher is not able to invoke Execute (dispatch the first work item) before the cancellation request comes in. Hence, when the Cancel work item lands in the scheduler work queue, the Execute work item will still also be in the work queue. The Execute work item has been logically overtaken because the enqueue of the Cancel work item moves the activity to the Canceling state. When the Execute work item is dequeued by the scheduler, it is ignored (and is not dispatched) because the target activity is already in the Canceling state.

Now let's consider the following WF program:

 <ChangedMyMind... >   <Trace x:Name="e1" /> </ChangedMyMind> 


Let's assume that the trace activity prints a message when its Execute method and Cancel method is actually called:

 using System; using System.Workflow.ComponentModel; public class Trace : Activity {   protected override ActivityExecutionStatus Execute(     ActivityExecutionContext context)   {     Console.WriteLine("Trace.Execute");     return ActivityExecutionStatus.Closed;   }   protected override ActivityExecutionStatus Cancel(     ActivityExecutionContext context)   {     Console.WriteLine("Trace.Cancel");     return ActivityExecutionStatus.Closed;   } } 


When the WF program executes, we will see the following output:

 e1 is Initialized : None e1 is Executing : None e1 is Canceling : None Trace.Cancel e1 is Closed : Uninitialized 


The TRace activity never has the work item for invocation of its Execute method dispatched. Because it moves to the Canceling state before its Execute execution handler is dispatched, only the Cancel method is invoked.

When you write activities that have custom cancellation logic, it is probably not a bad idea to use a pathological composite activity (like the one shown previously) as a test case to help ensure the correctness of your cancellation logic.

Composite Activity Cancellation

We will now return to the execution logic of Interleave to see how it responds to cancellation. Listing 4.15 shows the implementation of its Cancel method. As you can see, the Cancel method of Interleave, unlike that of Wait, has no cleanup of private data to perform. It does, however, need to propagate the signal to cancel to any executing child activities because Interleave will not be able to report the completion of its cancellation logic if it has child activities that are still executing. The Interleave activity will therefore remain in the Canceling state until all previously executing child activities are canceled.

Listing 4.15. Interleave Activity's Cancellation Logic

 using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities {   public class Interleave : CompositeActivity   {     ...     protected override ActivityExecutionStatus Cancel(       ActivityExecutionContext context)     {       bool okToClose = true;       foreach (Activity child in EnabledActivities)       {         ActivityExecutionStatus status = child.ExecutionStatus;         if (status == ActivityExecutionStatus.Executing)         {           context.CancelActivity(child);           okToClose = false;         }         else if ((status != ActivityExecutionStatus.Closed)           && (status != ActivityExecutionStatus.Initialized))         {           okToClose = false;         }       }       if (okToClose)         return ActivityExecutionStatus.Closed;       return ActivityExecutionStatus.Canceling;     }   } } 


The cancellation logic shown in Listing 4.15 is correct for many composite activities, not just for Interleave. For example, it is a correct implementation for Sequence (and we will assume that the logic of our Sequence activity is updated to reflect this), though the cancellation logic for Sequence could be written slightly differently because at most one child activity of Sequence can be in the Executing state at a time. Any child activities in the Executing state are asked to cancel. When all child activities are in either the Initialized state or Closed state, then the composite activity may report its completion.

We've already alluded to the fact that just as for activity execution, activity cancellation is a scheduled request made via AEC. The Interleave activity (and the Sequence activity) must subscribe to the Closed event of any child activity for which it invokes the CancelActivity method of AEC. This in turn requires us to modify the logic of ContinueAt. We must selectively act upon the Closed event depending upon whether the composite activity is in the Executing state or the Canceling state. In other words, we are using the same bookmark resumption point to handle the Closed event that is raised by child activities that complete and also the Closed event that is raised by child activities that are canceled.

It is possible for a composite activity (call it "A") to detect that it is in the Canceling state before its Cancel method is invoked. This will happen if its parent activity schedules "A" for cancellation, but the notification for the Closed event of a child activity of "A" reaches "A" before the dispatch of its Cancel method.

The following code snippet shows a cancellation-aware implementation of Interleave.ContinueAt:

 public class Interleave : CompositeActivity {   // other members same as earlier   ...   void ContinueAt(object sender,     ActivityExecutionStatusChangedEventArgs e)   {     e.Activity.Closed -= ContinueAt;     ActivityExecutionContext context =       sender as ActivityExecutionContext;     if (ExecutionStatus == ActivityExecutionStatus.Executing)     {       foreach (Activity child in EnabledActivities)       {         if (child.ExecutionStatus !=           ActivityExecutionStatus.Initialized &&             child.ExecutionStatus !=           ActivityExecutionStatus.Closed)           return;       }       context.CloseActivity();     }     else // canceling     {       bool okToClose = true;       foreach (Activity child in EnabledActivities)       {         ActivityExecutionStatus status = child.ExecutionStatus;         if (status == ActivityExecutionStatus.Executing)         {           // This happens if invocation of our Cancel method           // has been scheduled but is still sitting in the           // scheduler work queue           okToClose = false;           context.CancelActivity(child);         }         else if ((status != ActivityExecutionStatus.Closed) &&           (status != ActivityExecutionStatus.Initialized))         {           okToClose = false;         }       }       if (okToClose)         context.CloseActivity();     }   } } 


Early Completion

The AnyOneWillDo activity that we developed earlier in this topic is so similar to Interleave we might consider just building the early completion capability directly into Interleave. While we are at it, we can also generalize how the completion condition is expressed. Imagine a variant of the Interleave activity that carries a property, PercentMustComplete, that indicates the percentage of child activities that must complete in order for the Interleave to be considered complete. If such an Interleave activity is given six child activities, and PercentMustComplete is set to 50%, only three child activities need to complete before the Interleave activity can report its completion:

 <Interleave PercentMustComplete="50%" ...>   <A />   <B />   <C />   <D />   <E />   <F /> </Interleave> 


The Execute method of Interleave remains as we wrote it in Chapter 3; all child activities are immediately scheduled for execution. As the child activities complete their execution, the Interleave is notified via the ContinueAt callback. In the ContinueAt method, the Interleave can now check to see whether the PercentMustComplete threshold has been met (or exceeded), and based upon this check, decide whether to report its own completion.

It is easy to write many variants of Interleave in which the variation is confined to the criterion used to determine when a sufficient number of child activities have completed their execution. One way to generalize this idea of completion condition is to make it a customizable feature of the composite activity. It is easy to write a variant of Interleave that carries a property whose type is ActivityCondition (just like the While activity we developed has a property of type ActivityCondition).

The Interleave activity can evaluate this condition in its Execute method (in case no child activities need be executed), and again whenever the ContinueAt method is invoked, in order to decide when to complete. You can also imagine a variant of Sequence, or in fact a variant of just about any composite activity, that provides an early completion capability. Composite activities that create subordinate execution contexts (such as InterleavedForEach) need to perform cancellation of the activity instances within subordinate execution contexts as part of their early completion.

The actual syntax of condition types (derivatives of ActivityCondition) supported natively by WF is discussed in Chapter 8. Here we will use a stylized form of condition writing to communicate our intent. A completion condition allows composite activities to capture real-world control flow in a very flexible way:

 <Interleave CompletionCondition="A OR (B AND (C OR D))" >   <A />   <B />   <C />   <D /> </Interleave> 


This gives us the ability to model real-world processes that are a bit more involved in their completion logic than the simple car sale scenario we started with.

Cancellation Handlers

The composite activities we have written are general purpose in nature. In other words, when a Sequence activity or Interleave activity is used in a WF program, the developer of the program decides which child activities should be added to that occurrence of the composite activity. Although the execution logic of Sequence itself is unchanging, there are an infinite number of ways to use it because it accepts any list of child activities you wish to give it.

In this way, it may be said that both the developer of Sequence and the WF program developer who uses a Sequence activity have a say in what any particular occurrence of Sequence actually accomplishes. The WF programming model extends this same idea to activity cancellation, with the idea of a cancellation handler.

The concept is quite simple. When a composite activity is canceled by its parent, its Cancel method is scheduled for execution. As we know, this immediately moves the activity into the Canceling state. The composite activity remains in this state until all its child activities are quiet, at which time the composite activity reports the completion of its cancellation logic and moves to the Closed state. This much we have already covered. The extra step we are introducing here is that if the composite activity has an associated cancellation handler, that handler is executed as a final step prior to the activity's transition to the Closed state.

Any composite activity (unless its validation logic is written to explicitly prevent this) is allowed to have one special child activity of type CancellationHandlerActivity. This activity type is defined in the System.Workflow.ComponentModel namespace and is shown in Listing 4.16. The purpose of a cancellation handler is to allow the WF program developer to model what should happen in the case of an activity cancellation.

Listing 4.16. CancellationHandlerActivity

 namespace System.Workflow.ComponentModel {   public sealed class CancellationHandlerActivity : CompositeActivity   {     public CancellationHandlerActivity();     public CancellationHandlerActivity(string name);   } } 


Because CancellationHandlerActivity is a composite activity, you may add whatever activities are required to represent the necessary cancellation logic. The child activities of a CancellationHandlerActivity will execute in sequential order.

In order to help prevent a composite activity (say, Sequence) from executing its CancellationHandlerActivity as part of normal execution logic, the EnabledActivities collection of CompositeActivity will never include the composite activity's cancellation handler. Only the WF runtime will schedule the execution of a cancellation handler.

Let's take a look at an example to see how CancellationHandlerActivity can be used. Listing 4.17 shows a simple WF program that starts two timers simultaneously.

Listing 4.17. CancellationHandlerActivity

 <Interleave x:Name="i1" CompletionCondition="seq1 OR seq2" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow">   <Sequence x:Name="seq1">     <Wait Duration="00:01:00" x:Name="delay1" />     <wf:CancellationHandlerActivity x:Name="ch1">       <WriteLine x:Name="w1" Text="Cancelling seq1" />     </wf:CancellationHandlerActivity>   </Sequence>   <Sequence x:Name="seq2">     <Wait Duration="00:00:10" x:Name="delay2" />     <wf:CancellationHandlerActivity x:Name="ch2">       <WriteLine x:Name="w2" Text="Cancelling seq2" />     </wf:CancellationHandlerActivity>   </Sequence> </Interleave> 


The Interleave activity carries a completion condition (expressed in stylized form), which says that either child activity must complete in order for the Interleave to complete. When the first timer fires, the corresponding Wait activity ("delay2" based on the Duration values in the example WF program) is notified and then reports its completion. Sequence "seq2" will be notified of the completion of "delay2", which will cause "seq2" to report its completion. When the Interleave activity is notified of the completion of "seq2", it will evaluate the completion condition and decide that it can report its own completion. Before doing this, though, it must cancel the execution of the other Sequence, "seq1". When "seq1" is canceled, it will propagate the cancellation to "delay1". After "delay1" cancels its timer and reports its cancellation, the cancellation handler associated with "seq1" will be scheduled for execution. Only when this cancellation handler moves to the Closed state will "seq1" finally move to the Closed state.

Figure 4.8 illustrates the same sequence of operations in diagrammatic form.

Figure 4.8. Sequence diagram for Listing 4.18


To summarize, we have seen that activity cancellation can be a commonindeed, essentialpart of normal WF program execution. When a set of activities is executed in an interleaved manner, it is often the case that only some subset of these activities need to complete in order for the goal of that set of activities to be realized. The other in-progress activities must be canceled. Activity developers can add cancellation logic to their activities, and WF program developers can also have a say in what happens during cancellation by using a cancellation handler.




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