Saturday, October 14, 2006 8:48 PM bart

WF - Working with Persistence Services

Introduction

In the past, I've been writing about WF quite a bit. If you haven't read these articles already, you might find it interesting to check these out first before digging into this one. That being said, this post will focus on persistence services (also known as state persistence services).

Why persistence?

Before we dive deeper in the technical details, let's answer a crucial question: why persistence? As you probably know by now, workflows can be defined (at least partially) as a great tool to visualize either human workflow (e.g. approval processes) or machine workflow (e.g. business integration scenarios). Instead of writing procedural code, one creates a flow diagram or state diagram that represents the underlying logic that gets managed by the workflow runtime and engine. Read my WF introduction post for more information on this.

A common question is why one would prefer to use workflow instead of classic procedural code. There are a lot of answers to the question, one of which relates to the graphical aspect of a workflow that makes it more approachable and easier to understand. Another good answer is the support for scenarios that would end up with a rather high level complexity in procedural code, like parallel execution, data exchange with other components, transactional support, etc. Yet another one - the one that will interest us most - is a set of runtime services available to cope with more complex situations, including persistence services.

Beside the distinction of state workflows and sequential workflows and the distinction between machine workflows and human workflows, one can also draw a line between long-running and non-long-running workflows. It's not atypical (even more, rather typical) for workflows to be long-running. For example, approval processes will require human interaction by one or more parties to continue the workflow execution. Yet another example is a business integration application that waits for external data to arrive. In such a case we say a workflow to be idled. Needless to say, it would be a waste of resources (and even unreliable) to keep state of an idle workflow in memory. That's why we need persistence services.

The basics: getting idle

We'll start by taking a look at a workflow becoming idle. Start by creating a simple sequential workflow:

To force a workflow getting idle, we can use the DelayActivity. So, define the following workflow, containing a DelayActivity with the delay set to 10 seconds:

Next, define the ExecuteCode event handler for both CodeActivity activities as follows:

private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
   Console.ForegroundColor = ConsoleColor
.Green;
   Console.WriteLine("CodeActivity1 speaking"
);
   Console
.ResetColor();
}

private void codeActivity2_ExecuteCode(object sender, EventArgs
e)
{
   Console.ForegroundColor = ConsoleColor
.Green;
   Console.WriteLine("CodeActivity2 speaking"
);
   Console
.ResetColor();
}

When you launch this workflow application, no surprises will happen. At least not visibly (yet). The workflow will be launched, the "CodeActivity1 speaking" message will be printed out, the workflow will pause for 10 seconds, and then "CodeActivity2 speaking" will appear. Behind the scenes however, the workflow is idled when the delay activity is started. To visualize this, go to the host application's Program.cs file and hook in an event handler for the WorkflowIdled event as show below:

class Program
{
   static void Main(string
[] args)
   {
      using(WorkflowRuntime workflowRuntime = new WorkflowRuntime
())
      {
         AutoResetEvent waitHandle = new AutoResetEvent(false
);
         workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs
e) {waitHandle.Set();};
         workflowRuntime.WorkflowTerminated +=
delegate(object sender, WorkflowTerminatedEventArgs
e)
         {
            Console
.WriteLine(e.Exception.Message);
            waitHandle.Set();
         };
         workflowRuntime.WorkflowIdled +=
new EventHandler<WorkflowEventArgs
>(workflowRuntime_WorkflowIdled);

         WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(PersistenceDemo.Workflow1
));
         instance.Start();

         waitHandle.WaitOne();
      }
   }

   static void workflowRuntime_WorkflowIdled(object sender, WorkflowEventArgs
e)
   {
      Console.WriteLine("Idled " + e.WorkflowInstance.InstanceId);

   }

}

Run the application now. It should print the following before it completes (i.e. screenshot taken during the delay activity execution):

How persistence works

When a workflow gets idle, it can be persisted, so that it can be unloaded from memory. A persistence service inherits from the System.Workflow.Hosting.WorkflowPersistenceService base class that's used by the workflow engine to perform persistence. I recommend you to check out this class. Workflow comes with a SQL Server driven persistence service out of the box, defined in System.Workflow.Hosting.SqlWorkflowPersistenceService.

When a workflow gets idle, the system checks in the registered services to find a persistence service, if any. If it finds when, the persistence service gets called to perform the persistence. Next, the system uses the persistence information to find out about the "next timer expiration" to wake up the idled workflow instance when required, so that it can continue its job executing.

Setting up persistence

The first step to set up persistence using SQL Server (2000, MSDE, 2005, Express) is to create the persistence database. In this post, I'll use SQL Server 2005 Express Edition to illustrate the persistence service. You can download the software over here. Don't forget to install SQL Server Management Studio Express too.

  1. Open SQL Server Management Studio Express and connect to the server (typically localhost\SQLEXPRESS):


  2. Next, create a new database called SqlPersistenceDemo, either by invoking CREATE DATABASE or by using the tools:


  3. Now it's time to define the database by executing the WF .sql scripts provided in %windir%\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN:


  4. First, execute SqlPersistenceService_Schema.sql, then execute SqlPersistenceService_Logic.sql. Make sure to use the right database when you execute the scripts (SqlPersistenceDemo):


  5. You're ready. The persistence database should contain the following tables and sprocs:

Now, we can alter the code to support persistence. This is done in the host application by creating an instance of the SqlWorkflowPersistence class and registering it as a service with the workflow runtime:

workflowRuntime.AddService(
   new SqlWorkflowPersistenceService(
      "Initial Catalog=SqlPersistenceDemo;Data Source=localhost\\SQLEXPRESS;Integrated Security=SSPI;"
,
      true
,
      TimeSpan
.FromHours(1.0),
      TimeSpan
.FromSeconds(5.0)
   )
);

The first parameter is straightforward and points to the database. In this case I'm using the SQLEXPRESS instance and connecting using windows integrated authentication. The second parameter is called "unloadOnIdle" which unloads the workflow when it's idled. This is the point in time where persistence happens. The third parameter isn't relevant for our elaboration right now and is used when multiple hosts can hydrate/dehydrate workflow instances from the database (it sets the ownership duration for the host, so that if another host tries to load the instance during that interval, it doesn't succeed in doing so and an exception will be thrown). The last parameter specifies the loading interval used to check whether a workflow instance has to be dehydrated (i.e. loaded from the database). We set this value to 5 seconds for the sake of the demo.

Tip: Check out SqlConnectionStringBuilder is you didn't do so yet and you want a more structured approach to create a connection string:

SqlConnectionStringBuilder sb = new SqlConnectionStringBuilder();
sb.IntegratedSecurity =
true
;
sb.DataSource =
"localhost\\SQLEXPRESS"
;
sb.InitialCatalog =
"SqlPersistenceDemo"
;
string connString = sb.ToString();

Testing it

Time to test. But before we do so, let's hook in another set of event handlers to find out about unload/load/persist events for the workflow:

   workflowRuntime.WorkflowIdled += new EventHandler<WorkflowEventArgs>(workflowRuntime_WorkflowIdled);
   workflowRuntime.WorkflowLoaded +=
new EventHandler<WorkflowEventArgs>(workflowRuntime_WorkflowLoaded);
   workflowRuntime.WorkflowUnloaded +=
new EventHandler<WorkflowEventArgs
>(workflowRuntime_WorkflowUnloaded);
   workflowRuntime.WorkflowPersisted +=
new EventHandler<WorkflowEventArgs
>(workflowRuntime_WorkflowPersisted);

...

static void workflowRuntime_WorkflowPersisted(object sender, WorkflowEventArgs e)
{
   Console.WriteLine("Persisted {0} on {1}", e.WorkflowInstance.InstanceId, DateTime.Now.ToUniversalTime());
}

static void workflowRuntime_WorkflowUnloaded(object sender, WorkflowEventArgs e)
{
   Console.WriteLine("Unloaded {0} on {1}", e.WorkflowInstance.InstanceId, DateTime.Now.ToUniversalTime());
}

static void workflowRuntime_WorkflowLoaded(object sender, WorkflowEventArgs e)
{
   Console.WriteLine("Loaded {0} on {1}", e.WorkflowInstance.InstanceId, DateTime.Now.ToUniversalTime());
}

static
void workflowRuntime_WorkflowIdled(object sender, WorkflowEventArgs e)
{
   Console.WriteLine("Idled {0} on {1}", e.WorkflowInstance.InstanceId, DateTime.Now.ToUniversalTime());
}

Before we press the magic F5 button, there's one more thing to do. Go to SQL Server Management Studio Express and create a new query:

use SqlPersistenceDemo

exec dbo.
RetrieveAllInstanceDescriptions
select * from dbo.InstanceState

Basically, the two are almost identical. Generally, I'd recommend to rely on the stored procedures to retrieve information about the workflow instances in progress, but let's just illustrate both:

Now, run the workflow application. You'll see something like this:

As you can see, the idled event is followed by unloading the workflow, thanks to the persistence service (cf. the second "unloadOnIdle" parameter in the constructor call of SqlPersistenceService). Next, when the workflow is unloaded, it's time to persist the workflow instance in the database. While the application is running you have 10 seconds to re-run the query to see this:

If you want to understand how a workflow is persisted, check out the Windows SDK documentation on writing a custom persistence service (search for "Creating Custom Persistence Services"). All of this works using serialization of the runtime workflow instance state.

Finally, the workflow will be reloaded and execution continues:

Notice execution didn't proceed after 10 seconds, but it took 15 seconds instead. The reason for this is the specified loading interval (cf. last parameter to the SqlPersistenceService constructor parameter). Based on this interval, the runtime checks whether a timer has expired amongst the persisted workflow instances.

Tip: Lower the delay's TimeoutDuration to 9 seconds and see what happens. Keep in mind little delays incurred by the runtime to do the persistence when analyzing the results.

Conclusion

In this post, we've seen the basics of the workflow state persistence services. Thanks to this service, long-running workflows can be implemented efficiently without you having to worry much about the persistence itself. In other words, services like persistence services free developers from the burden to cope with error-prone tasks and recurring patterns like implementing persistence. In the next workflow post, I'll focus on the tracking service, so keep an eye on my RSS feed.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: ,

Comments

# Windows Workflow Foundation Resources

Tuesday, October 17, 2006 7:04 PM by while(availableTime>0) {

Since I&#180;ve been working with Windows Workflow Foundation (Project BHAL), I&#180;ve gathered quite a list of...

# The Roadshow has Begun!

Saturday, March 03, 2007 8:39 AM by Chris Bowen's Blog

We'd like to extend a big thank you to everyone who attended the first two stops of Bob &amp; Chris'

# The Roadshow Has Begun!

Saturday, March 03, 2007 8:43 AM by RSS It All

We&#39;d like to extend a big thank you to everyone who attended the first two stops of Bob &amp; Chris&#39;