Passivating a WF Program Instance


Passivation is the process by which a WF program instance is persisted to a (typically durable) storage medium and removed from memory. At a later point in time, the instance can be brought back into memory and its execution is resumed. A passivated WF program instance can resume its execution in a different WF runtime, perhaps in a different CLR application domain (say, after a machine has crashed and restarted), or even on a different machine.

The WF runtime relies upon a runtime service called the persistence service in order to save and load instances to and from storage. It is possible to use the WF runtime with passivation disabled by not providing a persistence service. Although there is no default persistence service prescribed by the WF runtime, the System.Workflow.Runtime.Hosting namespace does include a SqlWorkflowPersistenceService, which we will use in some of our examples.

The standard reason for passivating a WF program instance is that the instance has become idle. A WF program instance is considered idle when there are no items in the work queue of its scheduler; no forward progress can be made without some external stimulus. When this occurs, the thread that the WF runtime obtained via WorkflowSchedulerService and used to dispatch work items is returned to the host. The WF program instance still lingers in memory, though. At this point, the WF runtime will raise the WorkflowRuntime.WorkflowIdled event. The host application can cause passivation to occur whenever a WF program instance becomes idle by subscribing for this event and calling the Unload method of the Workflow-Instance,[1] as shown here:

[1] Because this is such a common pattern, you can automatically passivate idle WF program instances by giving the UnloadOnIdle property of SqlWorkflowPersistenceService a value of TRue.

 using (WorkflowRuntime runtime = new WorkflowRuntime()) {   runtime.WorkflowIdled += delegate(object sender,     WorkflowEventArgs e)   {     e.WorkflowInstance.Unload();   };   ... } 


The host application need not wait until a WF program instance is idle to unload it. For example, if a machine is running low on memory, the host may decide to proactively unload running instances while their execution is in progress. In other situations, running instances may be suspended and then unloaded. In order to preserve transaction guarantees, passivation cannot occur while the execution of TRansactionScopeActivity (discussed in Chapter 6, "Transactions") is in progress. In such a situation, passivation will not occur until the transaction has completed.

As we have seen, WorkflowInstance.Unload is the method that the host application can call in order to passivate a WF program instance. WorkflowInstance.TryUnload differs from Unload in that it will succeed only if the instance is idle. If the instance is doing any work (the scheduler thread is active) when tryUnload is called, a value of false is returned and there is no effect on the instance.

Every WF program instance has a globally unique instance identifier associated with it. Thus, the host application can load a previously saved (passivated) WF program instance by calling WorkflowRuntime.GetWorkflow passing the required identifier. GetWorkflow causes the program instance to be loaded into memory from the store provided by the persistence service. If the host application already holds a WorkflowInstance for a passivated instance, the instance can be loaded by calling WorkflowInstance.Load, as shown here:

 using (WorkflowRuntime runtime = new WorkflowRuntime()) {   runtime.StartRuntime();   WorkflowInstance instance =     runtime.CreateWorkflow(type);   instance.Start();   ...   instance.Unload();   ...   instance.Load();   ... } 


There is no fundamental difference between WorkflowRuntime.GetWorkflow and WorkflowInstance.Load.

Typically, a passivated instance is loaded in order to deliver stimulus that has arrived from the external world:

 Guid handle = ... WorkflowInstance instance = runtime.GetWorkflow(handle); instance.EnqueueItem(...); 


WorkflowInstance.EnqueueItem enqueues an object in the specified WF program queue. This will typically trigger the resumption of a bookmark (the scheduling of an activity method) that was created by an activity that asked to be notified when data arrived in that WF program queue. This handoff of data from the host application to activities is illustrated later in this chapter and also explored fully (in the context of transactions) in Chapter 6.

As mentioned earlier, you can write a custom persistence service that utilizes whatever storage medium is appropriate for your solution. To do this, you extend an abstract base class, which is called WorkflowPersistenceService, and is shown in Listing 5.12.

Listing 5.12. WorkflowPersistenceService

 namespace System.Workflow.Runtime.Hosting {   public abstract class WorkflowPersistenceService :     WorkflowRuntimeService   {     protected static byte[] GetDefaultSerializedForm(       Activity activity);     protected static Activity RestoreFromDefaultSerializedForm(       byte[] activityBytes, Activity outerActivity);     protected internal abstract bool UnloadOnIdle(Activity activity);     protected internal abstract Activity LoadWorkflowInstanceState(       Guid instanceId);     protected internal abstract void SaveWorkflowInstanceState(       Activity rootActivity, bool unlock);     protected internal abstract void UnlockWorkflowInstanceState(       Activity rootActivity);     protected internal abstract Activity LoadCompletedContextActivity(       Guid scopeId, Activity outerActivity);     protected internal abstract void SaveCompletedContextActivity(       Activity activity);   } } 


The GeTDefaultSerializedForm and RestoreFromDefaultSerializedForm methods are static helpers that translate an Activity object to and from a default binary form. These methods rely upon the Save and Load methods of Activity, which in turn utilize a System.Runtime.Serialization.Formatters.Binary. BinaryFormatter by default to perform the serialization. There are overloads of Save and Load that accept any IFormatter (defined in the System.Runtime. Serialization namespace) so that a custom persistence service can use a formatter of its choosing. The Save and Load methods of Activity will only work properly when called on a scheduler thread (the thread running the WF program instance). Persistence services are the primary user of Save and Load. The other place in which these methods are utilized is in the Clone method of Activity, which (because it relies on Save and Load) should also only be called on a WF scheduler thread.

The GetIsBlocked, GetSuspendOrTerminateInfo, and GetWorkflowStatus methods are static helpers for obtaining information about the specified WF program instance. The values of the WorkflowStatus enumeration (see Listing 5.21) are discussed later in the chapter.

The UnloadOnIdle method must be overridden by a custom persistence service. This method is called by the WF runtime to determine whether a given WF program instance should be automatically unloaded when it becomes idle.

The real work of a persistence service is performed by the SaveWorkflowInstanceState and LoadWorkflowInstanceState methods. A custom persistence service will override these methods and provide the logic for saving and loading a WF program instance to the storage medium of choice. Whereas these methods are responsible for saving and loading an entire instance, the Load-CompletedContextActivity and SaveCompletedContextActivity methods perform a similar function for an activity that is the root of a dynamically created ActivityExecutionContext. A custom persistence service is expected to throw a PersistenceException (defined in the System.Workflow.Runtime.Hosting namespace) if an error occurs during loading or saving.

In some deployments of WF, there may be multiple WF runtimes (for example, on different machines) sharing a single storage medium for WF program instances (for example, a dedicated SQL Server installation). In such cases, the WF runtime and the persistence service collaborate to support a locking mechanism for WF program instances. If a persistence service supports this kind of multiruntime environment, it must lock the instance when a successful call to LoadWorkflowInstanceState is made. The instance is now under the control of a specific WF runtime. Likewise, when SaveWorkflowInstanceState is called, a Boolean parameter named unlock indicates whether the instance should then be unlocked. Finally, the persistence service defines an UnlockWorkflowInstanceState method that explicitly unlocks a given instance without actually saving it.

Listing 5.13 shows a custom persistence service that uses the BinaryFormatter[2] that is defined in the System.Runtime.Serialization.Formatters.Binary namespace to save and load WF program instances to and from files on disk.

[2] Although WF runtime serialization architecture allows for plugging custom formatters, at the time of writing, only BinaryFomatter is fully supported by WF.

Listing 5.13. Custom Persistence Service

 using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters; using System.Runtime.Serialization.Formatters.Binary; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Serialization; using System.Workflow.Runtime; using System.Workflow.Runtime.Hosting; namespace EssentialWF.Services {   public class FilePersistenceService :     WorkflowPersistenceService   {     private string location;     private bool unloadOnIdle = false;     public string Location     {       get { return this.location; }     }     protected override bool UnloadOnIdle(Activity activity)     {       return this.unloadOnIdle;     }     public FilePersistenceService(string location)     {       this.location = location;     }     public FilePersistenceService(string location, bool unloadOnIdle)     {       this.location = location;       this.unloadOnIdle = unloadOnIdle;     }     string BuildFilePath(Guid ctxid)     {       return Path.Combine(this.Location, ctxid.ToString() + ".bin");     }     protected override Activity LoadCompletedContextActivity(       Guid ctxId, Activity outerActivity)     {       return Load(ctxId, outerActivity);     }     protected override Activity LoadWorkflowInstanceState(       Guid instanceId)     {       return Load(instanceId, null);     }     protected override void SaveCompletedContextActivity(       Activity ctxActivity)     {       this.Save(ctxActivity, true);     }     protected override void SaveWorkflowInstanceState(       Activity rootActivity, bool unlock)     {       this.Save(rootActivity, unlock);     }     void Save(Activity activity, bool unlock)     {       Guid ctxid = (Guid)activity.GetValue(         Activity.ActivityContextGuidProperty);       string filePath = this.BuildFilePath(ctxid);       if (File.Exists(filePath))         File.Delete(filePath);       using (FileStream fs = new FileStream(filePath,           FileMode.CreateNew))       {         IFormatter formatter = new BinaryFormatter();         formatter.SurrogateSelector =           ActivitySurrogateSelector.Default;         activity.Save(fs, formatter);       }       if (!unlock)         File.SetAttributes(filePath, FileAttributes.ReadOnly);     }     Activity Load(Guid ctxid, Activity outerActivity)     {       string filePath = this.BuildFilePath(ctxid);       using (FileStream fs = new FileStream(filePath,           FileMode.Open, FileAccess.Read, FileShare.Read))       {         fs.Seek(0, SeekOrigin.Begin);         IFormatter formatter =new BinaryFormatter();         formatter.SurrogateSelector =           ActivitySurrogateSelector.Default;         return Activity.Load(fs, outerActivity, formatter);       }     }     protected override void UnlockWorkflowInstanceState(       Activity rootActivity)     {       Guid ctxid = (Guid)rootActivity.GetValue(           Activity.ActivityContextGuidProperty);       string filePath = this.BuildFilePath(ctxid);       using (FileStream fs = new FileStream(filePath, FileMode.Open))         File.SetAttributes(filePath, FileAttributes.Normal);     }   } } 


Runtime Activity Serialization

As we learned in the previous section, the WF runtime can utilize a persistence service to store and retrieve WF program instances to and from a durable storage medium. The format used for runtime serialization should be optimized for efficiency (as opposed to, say, readability) and must also capture the state (values of activity object fields) of all activity instances within the executing WF program instance. For these reasons, the runtime serialization behavior of activities is not the same as the design-time serialization behavior of activities.

First, let's be clear about exactly what state must be serialized when a WF program instance is persisted to durable storage. The obvious data to serialize is the state of all activity instances in the WF program instance. As we learned earlier, there may be more than one activity instance for a given activity declaration, due to dynamic creation of execution contexts by a composite activity. Each activity instance is just a CLR object that contains fields representing the state of that activity instance. Typically, all of these fields must be serialized, except for dependency property fields that specify activity metadata properties (which will be discussed in Chapter 7). Metadata properties do not need to be serialized because, by definition, they are shared by multiple WF program instances and their values are held in the WF program prototype managed by the WF runtime. This is a big win for runtime serialization efficiency.

Serializing the state of activity instances is necessary, but not sufficient. The WF runtime must also store bookkeeping data (such as the items in the work queue of the WF scheduler for the given WF program instance, and the internal bookmarks that are managed by the WF runtime on behalf of subscribers to the Activity.Closed event) when a WF program instance is persisted. This data ensures that the WF program instance about to be persisted can be loaded from durable storage at a later time, possibly in a different CLR application domain, and have its execution resumed.

Durability is an intrinsic property of WF program instances, and the WF programming model ensures that all WF programs have default serialization behavior. The details of runtime activity serialization, discussed in the remainder of this section, can typically be ignored by WF program authors using just XAML (or other declarative formats). Activity type developers should be aware of how their activities will be serialized at runtime so that they can, if necessary, utilize standard runtime serialization techniquesbased on the types and services available in the System.Runtime.Serialization namespaceto improve serialization efficiency.

Surrogate-Based Serialization

The WF runtime serializes WF program instances, including all the activity instances within them, automatically. What this means is that you are not required to decorate activity types with the System.SerializableAttribute attribute.

 // This is not required [Serializable] public class Widget : Activity { ... } 


To achieve automatic serialization, the WF runtime uses what are known as serialization surrogates. A serialization surrogate is a type that is responsible for serializing objects of another type. A serialization surrogate must implement the ISerializationSurrogate interface, which is defined in the System.Runtime.Serialization namespace.

The WF runtime registers an internally implemented serialization surrogate for the System.Workflow.ComponentModel.DependencyObject type. Therefore, all derivatives of DependencyObject (including all activity types, because Activity inherits from DependencyObject) automatically have runtime serialization behavior. Details of serialization surrogates, and the types in the System.Runtime.Serialization namespace, are beyond the scope of this book but are discussed thoroughly in .NET Framework documentation.

The default serialization surrogate for activities relies upon System.SerializableAttribute to determine whether a field or dependency property of an activity is serializable. If the types of all of the fields and dependency properties declared by an activity type are marked [Serializable], then as an activity developer, there is nothing else you need to do to make runtime serialization work. The following code snippet shows an activity type that is, by default, fully serializable:

 public class CreateOrder : Activity {   public static readonly DependencyProperty SKUProperty     = DependencyProperty.Register("SKU",       typeof(string), typeof(CreateOrder));   public string SKU   {     get { return (string) GetValue(SKUProperty); }     set { SetValue(SKUProperty, value); }   }   private int quantity;   public int Quantity   {     get { return this.quantity; }     set { this.quantity = value; }   }   ... } 


The System.String and System.Int32 types are both marked with [Serializable] so the SKUProperty dependency property field and the quantity field are both automatically serializable by the activity serialization surrogate.

However, consider a modified CreateOrder activity type that contains a field of a type Order which is not marked with System.SerializableAttribute:

 public class CreateOrder : Activity {   public static readonly DependencyProperty OrderProperty =     DependencyProperty.Register("Order",       typeof(Order), typeof(CreateOrder));   public Order Order   {     get { return (Order) GetValue(OrderProperty); }     set { SetValue(OrderProperty, value); }   }   ... } public class Order {   private string sku;   private int quantity;   public Order(string sku, int quantity)   {     this.sku = sku;     this.quantity = quantity;   }   public string SKU   {     get { return this.sku; }     set { this.sku = value; }   }   public int Quantity   {     get { return this.quantity; }     set { this.quantity = value; }   } } 


A type is not considered serializable unless it is marked with [Serializable]. Because the Order type is not marked with [Serializable], running a WF program that contains a CreateOrder activity will result in a runtime serialization exception (when the WF program instance is serialized):

 A first chance exception of type 'System.Runtime.Serialization.Serialization- Exception' occurred in mscorlib.dll Additional information: Type 'Order' in Assembly 'Experiments, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable. 


The problem can be easily fixed by marking the Order type as serializable:

 [Serializable] public class Order {   ... } 


If certain fields of a type you develop should not participate in runtime serialization (for efficiency reasons), you can mark these fields with the System.NonSerialized-Attribute attribute:

 [Serializable] public class Order {   [NonSerialized]   private string sku;   ...  } 


In order to avoid serialization of a dependency property (that is not a metadata property), you must indicate this desired behavior when the dependency property is registered:

 public static readonly DependencyProperty OrderProperty =   DependencyProperty.Register("Order",     typeof(Order),     typeof(CreateOrder),     new PropertyMetadata(DependencyPropertyOptions.NonSerialized) ); 


In certain cases, marking a type with SerializableAttribute may not be ideal. Because [Serializable] and [NonSerialized] are static decorations, all instances of the Order type (as implemented previously) are serialized in exactly the same way. Sometimes you might need to conditionally serialize fields of an object, or perform other custom processing. Implementing System.Runtime.Serialization.ISerializable gives you this kind of flexibility, illustrated here for the Order type:

 [Serializable] public class Order : ISerializable {   public Order(SerializationInfo info, StreamingContext context)   {     this.sku = info.GetString("sku");     this.quantity = info.GetInt32("quantity");   }   void ISerializable.GetObjectData(SerializationInfo info,     StreamingContext context)   {     // custom serialization logic     info.AddValue("sku", ...);     ...   } } 


In the discussion so far, we are assuming that we have control over the source code of the Order type, and can therefore apply [Serializable] or implement ISerializable. There are situations, though, when you might utilize compiled types that do not have the serialization behavior that you require. In such cases, you can implement a custom serialization surrogate for a compiled type, whose behavior will effectively override whatever runtime serialization behavior was implemented by the author of the type.

Let's assume that our Order type is not marked with [Serializable] and does not implement ISerializable. To make the Order type serializable, we first implement a serialization surrogate:

 public class OrderSurrogate :   System.Runtime.Serialization.ISerializationSurrogate {   void ISerializationSurrogate.GetObjectData(object obj,     SerializationInfo info, StreamingContext context)   {     Order order = obj as Order;     if (order != null)     {       info.AddValue("sku", order.SKU);       info.AddValue("quantity", order.Quantity);     }   }   object ISerializationSurrogate.SetObjectData(object obj,     SerializationInfo info, StreamingContext context,     ISurrogateSelector selector)   {     Order order = obj as Order;     if (order != null)     {       order.SKU = info.GetString("sku");       order.Quantity = info.GetInt32("quantity");     }       return order;     }   } 


In order to have the OrderSurrogate type recognized by the WF runtime's serialization machinery, you need to write a serialization surrogate selector. A surrogate selector is a type that inherits from System.Runtime.Serialization.SurrogateSelector (or implements System.Runtime.Serialization.ISurrogate-Selector) and allows you to associate a serialization surrogate with the type that it is responsible for serializing, as shown here:

 public class OrderSurrogateSelector :   System.Runtime.Serialization.SurrogateSelector {   private OrderSurrogate surrogate = new OrderSurrogate();   public override ISerializationSurrogate GetSurrogate(     Type type, StreamingContext context,     out ISurrogateSelector selector)   {     if (type == typeof(Order))     {       selector = this;       return this.surrogate;     }     return base.GetSurrogate(type, context, out selector);   } } 


The final step is to register the custom surrogate selector with the WF runtime:

 OrderSurrogateSelector selector =   new OrderSurrogateSelector(); ActivitySurrogateSelector.Default.ChainSelector(selector); using (WorkflowRuntime runtime = new WorkflowRuntime()) {   ... } 


The ActivitySurrogateSelector type that is used to register custom surrogates is defined in the System.Workflow.ComponentModel.Serialization namespace and is shown in Listing 5.14.

Listing 5.14. ActivitySurrogateSelector

 namespace System.Workflow.ComponentModel.Serialization {   public sealed class ActivitySurrogateSelector : SurrogateSelector   {     public ActivitySurrogateSelector();     public static ActivitySurrogateSelector Default { get; }     public override ISerializationSurrogate GetSurrogate(       Type type,       StreamingContext context,       out ISurrogateSelector selector     );   } } 


There is a simple pattern that can help you reduce the serialization size of activities you develop. The WF runtime does not serialize dependency properties with a value of null. You can take advantage of this fact when you develop an activity type that defines private fields (that presumably hold data relevant only to the activity's execution and therefore must be serialized only while the activity is executing).

Clearly, we can develop an activity like this:

 public class Widget : Activity {   private string str;   ... } 


The field str will always be serialized, potentially long after a Widget activity has completed its execution. We can rewrite the Widget activity like this:

 public class Widget : Activity {   private static readonly DependencyProperty StrProperty =     DependencyProperty.Register("Str",       typeof(string),       typeof(Widget));   // private getter/setter for Str   protected override void Uninitialize(     IServiceProvider provider)   {     base.Uninitialize(provider);     this.SetValue(StrProperty, null);   }   ... } 


StrProperty is a private dependency property field that a Widget activity uses to keep track of some data while it is executing. When a Widget activity completes its execution, the data is not of any further use. By setting the value of the dependency property to null in the Uninitialize method, we avoid serialization of this data for all subsequent persistence points for the WF program instance. We could alternatively have used a standard string field, and set its value to null in the Uninitialize method, but the dependency property approach is nevertheless slightly more efficient (in terms of serialization size).




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