Fault Handling


The execution handlers that are invoked by the scheduler are just methods on activities, so each of them has the potential to throw an exception. An exception can occur in Execute, Cancel, ContinueAt, or any activity method that is scheduled for execution. If an exception does occur, and is not handled internally by the activity's code (which of course is free to employ try-catch constructs), then it is the scheduler that catches the exception (because it is the scheduler that invoked the method). When this happens, it indicates to the WF runtime that the activity that threw the exception cannot successfully complete the work it was asked to perform.

In the WF programming model, the handling of such an exception is an aspect of the activity automaton. Thus, exception handling in WF programs has an asynchronous flavor that exception handling in C# programs does not have. As we shall see, exceptions propagate asynchronously (via the scheduler work queue) and are therefore handled by activities asynchronously. It's important to keep this high-level characteristic of WF exception handling in mind as you write and debug activities and WF programs.

The Faulting State

The throwing of an exception by an activity is at some level comparable to an activity calling the CloseActivity method of AEC; both are signals from an activity to the WF runtime that indicate the activity's decision to transition from one state to another. In the case of an exception, the offending activity immediately moves to the Faulting state.

As shown in Figure 4.9, there are transitions to the Faulting state from both the Executing state and the Canceling state.

Figure 4.9. The Faulting state of the activity automaton


Activity initialization is a special case that we discussed in Chapter 3. The Initialize method of an activity is not a scheduled execution handler. Activity initialization occurs synchronously as part of the creation of a WF program instance.

If an exception is thrown by the Initialize method of an activity, the call to WorkflowRuntime.CreateWorkflow (within which activity initialization occurs) throws an exception indicating that the WF program instance failed to initialize properly.

A note about terminology: There is no notion of a fault in WF that is in any way different than a CLR exceptionthe terms are synonyms in WF. However, the term fault handling is favored in WF over exception handling because the mechanism by which faults (exceptions) are handled in WF does differ in important ways from the familiar exception handling constructs in CLR languages like C#. These differences are explored in the remainder of this section.

Figure 4.9 shows that after an activity enters the Faulting state, the only possible transition it can subsequently make is to the Closed state. When this transition occurs, the value of the activity's ExecutionResult property is Faulted. Figure 4.9 also implies that, if the Faulting state is like the Executing and Canceling states, an activity may remain in the Faulting state for an indefinite amount of time. Indeed this is the case. Only an activity can decide when it is appropriate to transition to the Closed state; this is true no matter whether an activity makes the transition to the Closed state via normal execution, cancellation, or the occurrence of a fault.

When an activity enters the Faulting state, its HandleFault method is scheduled by the WF runtime. The HandleFault method, like Execute and Cancel, is defined on the Activity class:

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


When the HandleFault execution handler is dispatched, it is expected that the activity will perform any cleanup work that is required prior to its transition to the Closed state. Just as with the Execute and Cancel execution handlers, the activity may (if the cleanup work is short-lived) return ActivityExecutionStatus.Closed from this method. If the cleanup work is long-lived, the activity returns ActivityExecutionStatus.Faulting and waits for required callbacks before ultimately calling ActivityExecutionContext.CloseActivity.

To illustrate the mechanics of an activity's transitions from the Executing state to the Faulting state to the Closed state, consider the activity shown in Listing 4.18, which throws an exception in its Execute method, and then another exception in its HandleFault method.

Listing 4.18. An Activity that Always Faults

 using System; using System.Workflow.ComponentModel; public class NeverSucceeds : Activity {   protected override ActivityExecutionStatus Execute(     ActivityExecutionContext context)   {     throw new InvalidOperationException("told you so");   }   private bool beenHereBefore = false;   protected override ActivityExecutionStatus HandleFault(     ActivityExecutionContext context, Exception exception)   {     Console.WriteLine(exception.Message);     if (beenHereBefore)       return ActivityExecutionStatus.Closed;     beenHereBefore = true;     throw new InvalidOperationException("second time");   } } 


If we run a WF program that consists of only a NeverSucceeds activity, we will see the following output at the console:

 told you so second time 


When the program starts, the Execute method of the NeverSucceeds activity is scheduled, which moves it into the Executing state. When the Execute method is invoked, an exception is of course thrown and is caught by the WF scheduler. This moves the activity into the Faulting state, and also enqueues a work item corresponding to the activity's HandleFault method. When HandleFault is dispatched, the Message property of the current exception ("told you so") is written to the console and, because the beenHereBefore variable value is false, a new exception is thrown. This second exception is also caught by the WF scheduler, and again the HandleFault method of the NeverSucceeds activity is scheduled for execution. The second time through, the beenHereBefore variable value is true (having been set during the first invocation of HandleFault) and so the activity reports its completion (after again printing the Message property of the current exception, which now is "second time"). This moves the activity from the Faulting state to the Closed state.

This example does illustrate that care must be taken to avoid an infinite loop in which exceptions occurring within HandleFault cause repeated scheduling of additional calls to HandleFault. The potential for infinite looping is a byproduct of the fact that an activity must be allowed to remain in the Faulting state for an indefinite amount of time while it performs any necessary cleanup work.

The HandleFault method allows an activity to perform cleanup work when an exception occurs. However, the behavior of HandleFault is not the same as that of the catch block familiar to C# programmers. The WF runtime will schedule the propagation of the exception (to the parent of the faulting activity) only when the faulting activity transitions to the Closed state.

The faulting activity can choose to suppress the propagation of an exception by setting the ActivityExecutionContext.CurrentExceptionProperty to null:

 protected override ActivityExecutionStatus HandleFault(   ActivityExecutionContext context, Exception exception) {   this.SetValue(ActivityExecutionContext.CurrentExceptionProperty, null);   return ActivityExecutionStatus.Closed; } 


The WF runtime automatically populates the AEC.CurrentExceptionProperty with the exception before calling HandleFault. It will clear the property when the faulting activity transitions to the Closed state. If the exception is not suppressed by the faulting activity, the exception will be made available to the parent activity.

The implementation of HandleFault that is provided by the Activity class simply returns ActivityExecutionStatus.Closed. Only when you need special cleanup to occur should you override this implementation. This is very similar to the situation for cancellation, which we previously discussed. In fact, in many cases it is useful to factor the cleanup logic for an activity like Wait into a helper method that is then called from both Cancel and HandleFault.

Composite Activity Fault Handling

As you might expect, things are a bit different for composite activities.

We saw previously that a composite activity will not be able to transition to the Closed state unless every child activity of that composite activity is either in the Initialized state or the Closed state. Therefore, when the HandleFault method of a composite activity is dispatched, the default implementation inherited from Activity will not be correct.

The implementation of the HandleFault method found in CompositeActivity, shown here, will call the Cancel method of the composite activity in order to ensure cancellation of all currently executing child activities:

 ActivityExecutionStatus s = this.Cancel(context); if (s == ActivityExecutionStatus.Canceling) {   return ActivityExecutionStatus.Faulting; } return s; 


As we saw in the previous section, it is critical that composite activities implement appropriate cancellation logic. The simple implementation of the Cancel method defined by Activity immediately returns a status of ActivityExecutionStatus.Closed. This will not work for composite activities, and can lead to an infinite loop due to the fault that will be thrown by the WF runtime when a composite activity tries to report its completion if it has child activities still doing work.

Just as with cancellation, the composite activity must wait for all active child activities to move to the Closed state before it can report its completion. In the case of a composite activity that is handling a fault, the call to the CloseActivity method of AEC will move the composite activity from the Faulting state to the Closed state.

A composite activity will therefore, by default, see its Cancel method called (from HandleFault) when a fault needs to be handled. It is expected that for many composite activities, the logic of Cancel corresponds exactly to what must happen during the handling of a fault; specifically, all child activities in the Executing state are canceled, and the composite activity's call to AEC.CloseActivity occurs only when all child activities are in either the Initialized state or the Closed state. The implementations of HandleFault in Activity and CompositeActivity are just a convenience, however, and can be overridden in cases where they do not provide the appropriate behavior.

Fault Propagation

Thus far, we have only discussed the throwing and handling of a fault in the context of the activity whose execution handler produced an exception. For this activity, there is a transition to the Faultingstate (from either the Executing state or the Canceling state) and a subsequent transition to the Closed state. But when this activity moves to the Closedstate, the fault can hardly be considered to have been handled. After all, the default implementations of HandleFault provided by Activity and CompositeActivity essentially just enable an activity to move to the Closed state from the Faulting state as quickly as possible.

What actually happens when an activity transitions to the Closed state from the Faulting state is that (unless the fault is suppressed using the technique outlined earlier) the WF runtime automatically propagates the exception one level up the activity hierarchy. Specifically, the WF runtime schedules a work item for the HandleFault method of the parent of the activity that faulted; when this occurs, the parent activity moves to the Faulting state. Here we find the key point of difference between fault handling in WF and exception handling in the CLR. The WF fault does not propagate until the faulting activity moves to the Closed state; as we know this might take an arbitrary amount of time during which other scheduled execution handlers in the same WF program instance will be dispatched. The execution of other activities proceeds, unaffected by the fact that a fault has occurred.

When the exception propagates one level up, the parent composite activity eventually has a work item for its HandleFault method dispatched. As expected, the fault-handling logic of this composite activity will cause the cancellation of any executing child activities. Only when all child activities are quiet will the composite activity move to the Closed state; when this occurs, the exception will propagate yet one more level up the activity tree. In this way, the WF fault-handling model provides for the orderly cleanup of work as the exception propagates up the tree, one composite activity at a time.

WF's execution model is stackless and is driven by the activity automaton. Because activities are organized hierarchically in a WF program, the WF runtime must propagate exceptions according to the constraints of the activity automaton; this ensures downward propagation (and, indeed, completion) of cancellation (orderly cleanup of work) prior to the upward propagation of an exception. The WF program developer must be aware of the innately asynchronous nature of WF program execution. This is especially true when the fault-handling capabilities of activities are utilized.

Fault Handlers

We saw earlier in this chapter that the WF runtime schedules the execution of a cancellation handler, if one is present, when a composite activity transitions from the Canceling state to the Closed state. In a similar fashion, a FaultHandlers-Activity, if present, is scheduled for execution as part of a composite activity's transition from the Faulting state to the Closed state.

A FaultHandlersActivity (note the plural) is just an ordered list of child activities of type FaultHandlerActivity. A FaultHandlerActivity allows the WF program developer to model the handling of a fault of a specific type much like a catch handler in C#.

The FaultHandlerActivity type is shown in Listing 4.19.

Listing 4.19. FaultHandlerActivity

 namespace System.Workflow.ComponentModel {   public sealed class FaultHandlerActivity : CompositeActivity   {     public Exception Fault { get; }     public Type FaultType { get; set; }     /* *** other members *** */   } } 


The execution logic of FaultHandlersActivity is responsible for finding the child FaultHandlerActivity that can handle the current fault. If one is found, that activity is scheduled for execution, and the composite activity (whose FaultHandlersActivity is being executed) remains in the Faulting state until the fault handler completes its execution. If no matching fault handler is found, the fault is propagated by the WF runtime to the next outer (parent) composite activity, and the composite activity handling the fault moves from the Faulting state to the Closed state.

When a FaultHandlerActivity executes, it sets the CurrentException-Property of the faulting composite activity to null in order to suppress the propagation of the (successfully handled) exception.

Unhandled Faults

If the root activity of a WF program moves from the Faultingstate to the Closedstate without having had a FaultHandlerActivity successfully handle the fault, the WF program terminates. The exception propagates to the application hosting the WF runtime in the form of an event.

The host application can subscribe to the WorkflowRuntime.WorkflowTerminated event to see the exception:

 using (WorkflowRuntime runtime = new WorkflowRuntime()) {   runtime.WorkflowTerminated += delegate(object sender,     WorkflowTerminatedEventArgs e)   {     Exception exception = e.Exception;     ...   }; ... } 


Modeled Faults

It is an inescapable fact that exceptions will sometimes be thrown by opaque activity code. Sometimes, though, it can be desirable to represent, or model, the throwing of a fault as an explicit part of a WF program.

A side effect of layering the WF runtime on top of the CLR is that the WF runtime must be able to recognize and deal with CLR exceptions raised, at the lowest level, via the Microsoft Intermediate Language (MSIL) throw instruction. Activities, after all, are compiled to MSIL instructions. Therefore, although the propagation of a fault and its eventual handling is governed by the laws of the WF runtime (and not the CLR's), the mechanism of throwing a fault is still the MSIL throw instruction.

In a more pristine architecture, wherein activities might be written with an exclusive reliance on WF APIs, WF could provide an API for activity writers to notify the WF runtime of the occurrence of a fault. This API would be used instead of the MSIL throw instruction. Rather than invent this duplicate API, though, WF pragmatically uses the throw statement of languages like C# as the mechanism by which activities signal a fault. Purists might object to the absence of a WF API for signaling a fault; this is an issue[1] that illustrates the design tradeoffs inherent in the building of a meta-runtime such as the WF runtime on top of the CLR.

[1] The practicalities of activity object constructors and the Dispose method is another that we encountered earlier.

That said, from the perspective of the WF program developer, it remains useful to hide this debate and provide an activity whose job is to raise a fault. An activity analog of the C# throw statement, a ThrowFault activity, is shown in Listing 4.20.

Listing 4.20. ThrowFault Activity

 using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities {   public class ThrowFault : Activity   {     public static readonly DependencyProperty FaultProperty =       DependencyProperty.Register("Fault",         typeof(Exception),         typeof(ThrowFault)     );     public Exception Fault     {       get { return GetValue(FaultProperty) as Exception; }       set { SetValue(FaultProperty, value); }     }     protected override ActivityExecutionStatus Execute(           ActivityExecutionContext context)   {       if (Fault == null)         throw new InvalidOperationException ("Null Fault");       throw Fault;     }   } } 


Setting the Fault property prior to the execution of a THRowFault activity is analogous to placing a reference to an object of type System.Exception on the stack prior to execution of the MSIL throw. If the exception object is null, an InvalidOperationException occurs.

The presence of THRowFault in a WF program indicates precisely where the program is predictably going to fault. We don't even have to run the program in Listing 4.21. Just by looking at it, you can predict the result. The program will write "hello, world" but will then terminate without writing "unreachable" to the console.

Listing 4.21. WF Program That Throws an Exception

 <Sequence x:Name="s1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">   <WriteLine x:Name="w1" Text="hello, world" />   <ThrowFault x:Name="throw1" />   <WriteLine x:Name="w2" Text="unreachable" /> </Sequence> 


The ThrowFault activity is useful as written, but (outside of using a default System.InvalidOperationException if no other is provided) it takes no responsibility for creating the exception that it throws. Although the WF program developer could certainly resort to code to create a specialized exception, which ThrowFault can then throw, this more or less defeats the purpose of modeling the throwing of the exception. In other words, it is preferable to declaratively specify both the creation and the throwing of an exception.

One way to go about this would be to write a CreateFault activity that manufactures a System.Exception object based upon certain inputs (such as the type of the exception to be created, and any constructor parameter values that should be used). The Fault property of a THRowFault activity could then be bound to the property of CreateFault that holds the manufactured exception.

This is a perfectly fine approach, but it makes reasoning about the types of exceptions thrown in a WF program a bit trickier because one must parse databinding expressions in order to determine the type of an exception that will be thrown. An alternative approach is to collapse both steps into a single activity that manufactures and throws an exception. A simple version of such an activity is shown in Listing 4.22.

Listing 4.22. THRowTypedFault Activity

 using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities {   public class ThrowTypedFault : Activity   {     public static readonly DependencyProperty FaultTypeProperty =       DependencyProperty.Register("FaultType",         typeof(System.Type),         typeof(ThrowTypedFault),         new PropertyMetadata(DependencyPropertyOptions.Metadata)       );     public static readonly DependencyProperty FaultMessageProperty =       DependencyProperty.Register("FaultMessage",         typeof(string),         typeof(ThrowTypedFault)       );     public Type FaultType     {       get { return GetValue(FaultTypeProperty) as Type; }       set { SetValue(FaultTypeProperty, value); }     }     public string FaultMessage     {       get { return GetValue(FaultMessageProperty) as string; }       set { SetValue(FaultMessageProperty, value); }     }     protected override ActivityExecutionStatus Execute(       ActivityExecutionContext context)     {       System.Reflection.ConstructorInfo c =         FaultType.GetConstructor(new Type[]           { typeof(System.String) }         );       Exception fault = c.Invoke(new object[] { FaultMessage } )         as Exception;       throw fault;     }   } } 


When it executes, the ThrowTypedFault activity creates a new exception object whose type is determined by the value of its FaultType metadata property. It is assumed that the exception type has a constructor that takes a single string parameter.

A ThrowTypedFault activity with a FaultType of InvalidOperationException and a FaultMessage of "nice try" is essentially equivalent to this C# statement:

 throw new InvalidOperationException("nice try"); 


It is easy to augment the implementation of ThrowTypedFault to make it more robust, and to make it meet the needs of your own WF programs. Appropriate validation logic (discussed in Chapter 7) can ensure that the FaultType property holds a type reference to a derivative of System.Exception. Additional properties (for example, an InnerException property that becomes yet another constructor parameter for the manufactured exception) might be added as well.

When the throwing and handling of exceptions are both modeled in a WF program, it becomes possible to reason quite usefully about the behavior of the program without looking at any activity code. The WF program in Listing 4.23 shows such a program.

Listing 4.23. WF Program with Exceptions

[View full width]

<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"> ... <ThrowTypedFault x:Name="throw1" FaultMessage="oops" FaultType="{x:Type System.InvalidOperationException}" /> ... <wf:FaultHandlersActivity> <wf:FaultHandlerActivity x:Name="fh1" FaultType="{x:Type System .InvalidOperationException}"> <WriteLine x:Name="w1" Text="{wf:ActivityBind fh1,Path=Fault.Message}" /> </wf:FaultHandlerActivity> <wf:FaultHandlerActivity x:Name="fh2" FaultType="{x:Type System.Exception}"> <ThrowFault x:Name="throw2" Fault="{wf:ActivityBind fh2,Path=Fault}" /> </wf:FaultHandlerActivity> </wf:FaultHandlersActivity> </Sequence>


Listing 4.23 is functionally similar to the following C# code:

 try {   ...   throw new InvalidOperationException("oops");   ... } catch (InvalidOperationException e) {   Console.WriteLine(e.Message); } catch (Exception e) {   throw e; } 


The sequence diagram in Figure 4.10 shows the sequence of execution for the WF program shown in Listing 4.23.

Figure 4.10. Sequence diagram for Listing 4.23


Keep in mind that the exception propagation mechanism of the WF runtime ensures orderly cleanup of executing activities beginning at the origin of the exception. In WF, both the execution and exception models are fundamentally asynchronous. Only when the exception bubbles up to the composite activity with a fault handler that is able to handle the fault (has a matching fault type) will the fault be handled.

When an exception occurs in a C# program, the transfer of control to the appropriate exception handler (however many nested program statements need to be traversed) is effectively instantaneous. In a WF program, propagation of a fault to a fault handler will take an indeterminate amount of time, depending upon the time it takes to clean up (cancel) running activities within the activity subtrees of the composite activities that must move from the Faulting state to the Closed state.

ThrowActivity

The System.Workflow.ComponentModel namespace includes an activity named THRowActivity (by convention, the names of activity types included with WF are suffixed with "Activity"). ThrowActivity combines the functionality of the two activities we've written in this chapter and thus is an easy way to model the throwing of exceptions in WF programs.

The following XAML snippets illustrate the usage of ThrowActivity:

 <ThrowActivity FaultType="{x:Type System.InvalidOperationException}" /> <ThrowActivity Fault="{wf:ActivityBind SomeActivity,Path=SomeProp}" /> 


You can also specify both FaultType and Fault when using ThrowActivity, in which case a validation error occurs if the value of the FaultType property and the type of the object referenced by the Fault property do not match. Where THRow-Activity does not meet the needs of your WF programs, though, hopefully you have seen in this section how easy it is to craft variants that deliver the functionality you require.




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