Windows Workflow Foundation (WF) ships with two out of box modes of passivation (also referred to as dehydration and unloading) of a workflow.  Passivation is the process by which a workflow's state is saved to the database AND the workflow is removed from memory for the time being.

Out of Box Support

Unfortunately our support is very binary and only related to the idle time of a workflow instance.  Our base persistence service class, WorkflowPersistenceService, defines a boolean method UnloadOnIdle which will be called when a workflow goes Idle.  If this method returns true, then the instance in question will be unloaded (persisted and removed from memory).  If, however, the method returns true then the instance will remain in-memory.

This is Less Than Ideal

WF has done a wonderful job providing extensibility points.  WF has also done a great job not enforcing our semantics on the user.  You want to write your own persistence then feel free ... serialize the workflows however you want and store them wherever you please.  You don't like either of our out of box threading models ... go ahead and write your own.  Our definition of ParallelActivity doesn't suite you ... create a parallel which does what you desire.

Here, however, we have locked you in.  We have a method which you must implement on the persistence service which returns true or false.  Either you want the idle instance to unload right then, or you don't.  But what about advanced policy?  What about "unload after 20 minutes of idle time", or "unload when the instance hits a specific point", or "unload after 20 minutes in memory regardless of idling"?

Custom Passivation Policy

Luckily, just because we locked you into having to think about our UnloadOnIdle concept, we didn't lock you into using it.  From here on out assume that we always return false from UnloadOnIdle.

Unload After 20 Minutes Idle

Let's create a new service which derives from WorkflowRuntimeService.  WorkflowRuntimeService is a base class which, on start up, will get a reference to the WorkflowRuntime so that you can safely access it.  In our custom service (UnloadIn20Service) we'll override the Start method, call base, and then subscribe to WorkflowIdled:

override void Start()
{
  base.Start();
  WorkflowRuntime.WorkflowIdled += OnWorkflowIdled;
}

Now, let's assume that we've written a collection with the following behavior:

  • Items in the list are pairs of workflow instance IDs and DateTimes
  • The list is sorted on the DateTime such that the earliest DateTime is at the head of the list
  • There is always an active timer which will expire at the DateTime specified by the item at the head of the list
  • There is an event TimeExpired (void(Guid))which is publicly exposed by the collection and raised any time the timer expires.
  • When the timer expires the item at the head of the list is removed

In short, a priority queue which notifies us when a timeout has expired for an instance.  Our OnWorkflowIdled now looks like (assume we have already subscribed to TimeExpired):

if (idledWorkflows.Contains(e.WorkflowInstance.InstanceId)
  idledWorkflows.Remove(e.WorkflowInstance.InstanceId);
idledWorkflows.Add(e.WorkflowInstance.InstanceId, DateTime.Now.AddMinutes(20));

Our OnTimeExpired handler:

WorkflowRuntime.GetWorkflow(instId).TryUnload();

Now, with a few lines of code we have created a service which we can add to the runtime which will manage unloading workflows after 20 minutes of idle time.  Note that by using TryUnload instead of Unload we are guaranteed that if the workflow is NOT currently idle then we will not actually unload the instance.  TryUnload will return false in that case and is a no-op.  Unload, in contrast, will block until the instance can be unloaded (not in a TransactionScopeActivity and the scheduler is not currently running an item - note that items are allowed to be on the scheduler queue) and then follow through with the passivation.

So, a quick walkthrough ... a workflow goes Idle and it is added to the list.  If it is the next one to "expire" then it a timer will be created by our list for that instance.  When the timer expires the service will attempt to unload the instance, but only if it is not currently executing.  In the case that it executes and then goes idle again before the 20 minutes are up then the old expiration is removed from the list and a new expiration is added.

Unload After 20 Minutes In-Memory

This is actually the same implementation as the last one except for two changes.  First, subscribe to the WorkflowLoaded event.  This will notify you when an instance is loaded into memory.  If you want newly created workflows to have the same behavior then you also should hook the handler to the WorkflowCreated event as this is another mechanism by which a workflow can find its way into memory.

Second, change the call to TryUnload to a call to Unload instead.  This will make sure that the instance is unloaded regardless of whether or not it has more processing which it could do.

Unload At  a Specific Point in the Workflow

Well, my fingers are getting tired, so I'm going to give this one a superficial overview.  In this case it would make sense to write a tracking service.  Many people have the incorrect view that writing a custom tracking service is too much work, but this is not the case.  In fact, I will put a post up on writing custom tracking services next time I write.

The technique here would be to create a tracking channel which is aware of 1) the instance being tracked (this data is passed when the tracking channel is requested) and 2) the workflow runtime itself.  The tracking channel could then wait for a specific message (ActivityTrackingRecord, WorkflowTrackingRecord, UserTrackingRecord) and cause the workflow runtime to unload the instance.

Conclusion

WF's extensibility model let's you do almost anything you can imagine.  Today I walked through a few examples of custom passivation policies which can be implemented using the framework provided by WF.  As noted above, next time I'll talk about writing a custom tracking service to dispel the myth that it is "too hard" ... I think the problem stems from the number of similar methods which must be overridden and the two class structure of a tracking service.  As always, questions and comments are welcome.