Working with the Workflow Foundation (3.5) Delay Activity (why won’t my expired workflow automatically reload?)

I was recently working with a customer who made the statement ‘Whenever I use a delay activity in my workflow, the workflow will automatically unload itself but will only reload into memory when a particular workflow runtime host is running.  It was our understanding that if a workflow had been unloaded due to a delay activity, that any runtime that had the persistence service added to it would just pick up the unloaded workflow whenever the timeout expired’. 

The answer to why this was happening was very easy but it was quite interesting to see how these workflows reacted to the runtime scenarios being used.  It was just interesting enough to think I would let others know in case they run into similar issues.

The scenario is to have two workflow sequential libraries and three hosts for these workflows. I placed all of these projects into one Visual Studio 2008 Solution.

image

WorkflowDelay1 – This project is a sequential workflow library with the following workflow:

image

This workflow has two code activities to write out to the console the workflow instance ID and a delay activity set to 30 seconds.  In order to run this workflow, I have a console based host project named WorkflowHostDelay1.  The code for the host is shown here (program.cs).  I also added a project reference to the host that points to the WorkflowDelay1 project:

    1:  using System;
    2:  using System.Collections.Generic;
    3:  using System.Linq;
    4:  using System.Text;
    5:  using System.Workflow.Runtime;
    6:  using System.Workflow.Runtime.Hosting;
    7:  using System.Threading;
    8:   
    9:  namespace WorkflowHostDelay1
   10:  {
   11:      class Program
   12:      {
   13:          static string connectionString = "Initial Catalog=TrackingStore;" + 
   14:              "Data Source=.\\SQLEXPRESS; Integrated Security=SSPI;";
   15:   
   16:          static void Main(string[] args)
   17:          {
   18:              using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
   19:              {
   20:                  AutoResetEvent waitHandle = new AutoResetEvent(false);
   21:                  workflowRuntime.WorkflowCompleted += 
   22:                      delegate(object sender, WorkflowCompletedEventArgs e) 
   23:                      { 
   24:                          waitHandle.Set(); 
   25:                      };
   26:                  workflowRuntime.WorkflowTerminated += 
   27:                      delegate(object sender, WorkflowTerminatedEventArgs e)
   28:                  {
   29:                      Console.WriteLine(e.Exception.Message);
   30:                      waitHandle.Set();
   31:                  };
   32:   
   33:                  workflowRuntime.AddService(new SqlWorkflowPersistenceService(connectionString, 
   34:                      true, new TimeSpan(0, 0, 30), new TimeSpan(0, 0, 10)));
   35:   
   36:                  workflowRuntime.StartRuntime();
   37:                  WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowDelay1.WFDelay1));
   38:                  instance.Start();
   39:                  
   40:                  Console.Title = "WorkflowDelay1";
   41:                  Console.BackgroundColor = ConsoleColor.DarkGreen;
   42:                  Console.ForegroundColor = ConsoleColor.Yellow;
   43:                  Console.Clear();
   44:                  Console.WriteLine();
   45:                  Console.WriteLine("WorkflowDelay1 host is running");
   46:                  Console.WriteLine("Press <enter> to exit.");
   47:                  Console.ReadLine();
   48:   
   49:                  waitHandle.WaitOne();
   50:   
   51:              }
   52:   
   53:          }
   54:      }
   55:  }

This host code simply adds the SQLWorkflowPersistenceService and creates/starts and instance of WorkflowDelay1.WFDelay1.  I’ve set the timespans for the OwnershipTimeout and LoadInterval shorter than normal just for testing.

My other workflow project, WorkflowDelay2 with a workflow type of WFDelay2 is very similar to the first workflow type except with one additional code activity added.  This workflow has a delay activity with a timeout of 1 minute:

image

The host code (WorkflowHostDelay2) is pretty much the same as the first set of host code.  The only difference is the workflow type being started.  I added a project reference to this host to point to the WorkflowDelay2 project.

    1:  using System;
    2:  using System.Collections.Generic;
    3:  using System.Linq;
    4:  using System.Text;
    5:  using System.Workflow.Runtime;
    6:  using System.Workflow.Runtime.Hosting;
    7:  using System.Threading;
    8:   
    9:  namespace WorkflowHostDelay2
   10:  {
   11:      class Program
   12:      {
   13:          static string connectionString = "Initial Catalog=TrackingStore;" + 
   14:              "Data Source=.\\SQLEXPRESS; Integrated Security=SSPI;";
   15:   
   16:          static void Main(string[] args)
   17:          {
   18:              using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
   19:              {
   20:                  AutoResetEvent waitHandle = new AutoResetEvent(false);
   21:                  workflowRuntime.WorkflowCompleted += 
   22:                      delegate(object sender, WorkflowCompletedEventArgs e) 
   23:                      { 
   24:                          waitHandle.Set(); 
   25:                      };
   26:                  workflowRuntime.WorkflowTerminated += 
   27:                      delegate(object sender, WorkflowTerminatedEventArgs e)
   28:                  {
   29:                      Console.WriteLine(e.Exception.Message);
   30:                      waitHandle.Set();
   31:                  };
   32:   
   33:                  workflowRuntime.AddService(new SqlWorkflowPersistenceService(connectionString, 
   34:                      true, new TimeSpan(0, 0, 30), new TimeSpan(0, 0, 10)));
   35:                  workflowRuntime.StartRuntime();
   36:                  WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowDelay2.WFDelay2));
   37:                  instance.Start();
   38:                  
   39:                  Console.Title = "WorkflowDelay2";
   40:                  Console.BackgroundColor = ConsoleColor.DarkGreen;
   41:                  Console.ForegroundColor = ConsoleColor.Yellow;
   42:                  Console.Clear();
   43:                  Console.WriteLine();
   44:                  Console.WriteLine("WorkflowDelay2 host is running");
   45:                  Console.WriteLine("Press <enter> to exit.");
   46:                  Console.ReadLine();
   47:   
   48:                  waitHandle.WaitOne();
   49:   
   50:              }
   51:   
   52:          }
   53:      }
   54:  }

 

So how do we test how the delay activities work?  What I am going to do, is start up both WorkflowHostDelay1 and WorkflowHostDelay2 long enough for the workflows to start and then be unloaded into the persistence InstanceState table.  Once I see that the workflows are persisted, I will shut down the console based hosts which means that the workflows will timeout while sitting in the database.

After I do this, taking a snapshot of the InstanceState table I see:

image

You can see here that these workflows are not owned by anyone at this point.

What I wanted to do next was start up a new instance of a workflow runtime (any runtime) with a persistence service and see what would happen with the workflows in the database.  I created another host project named HostWithOnlyaRuntime that looks like this:

    1:  using System;
    2:  using System.Collections.Generic;
    3:  using System.Linq;
    4:  using System.Text;
    5:  using System.Workflow.Runtime;
    6:  using System.Workflow.Runtime.Hosting;
    7:  using System.Threading;
    8:   
    9:  namespace HostWithOnlyaRuntime
   10:  {
   11:      class Program
   12:      {
   13:          static string connectionString = "Initial Catalog=TrackingStore;" + 
   14:              "Data Source=.\\SQLEXPRESS; Integrated Security=SSPI;";
   15:   
   16:          static void Main(string[] args)
   17:          {
   18:              using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
   19:              {
   20:                  //AutoResetEvent waitHandle = new AutoResetEvent(false);
   21:                  
   22:                  workflowRuntime.AddService(new SqlWorkflowPersistenceService(connectionString, 
   23:                      true, new TimeSpan(0, 0, 30), new TimeSpan(0, 0, 10)));
   24:                  workflowRuntime.StartRuntime();
   25:   
   26:                  Console.Title = "HostWithOnlyaRuntimeowDelay2";
   27:                  Console.BackgroundColor = ConsoleColor.White;
   28:                  Console.ForegroundColor = ConsoleColor.Black;
   29:                  Console.Clear();
   30:                  Console.WriteLine();
   31:                  Console.WriteLine("HostWithOnlyaRuntimeowDelay2 host is running");
   32:                  Console.WriteLine("Press <enter> to exit.");
   33:                  Console.ReadLine();
   34:   
   35:                  workflowRuntime.StopRuntime();
   36:   
   37:                  //waitHandle.WaitOne();
   38:   
   39:              }
   40:   
   41:          }
   42:      }
   43:  }

This code just starts up the runtime, adds the persistence service and then starts the runtime.  At this point, if I look at my records in the database I see that although it appears a have a new ownerID and it is locked until a certain point in time, the workflows themselves do not actually ever reload.

image

As long as I have this host running, these records will remain locked, which means no other host can start the workflows either.

To prove this point, comment out the code to start up a new workflow in WorkflowHostDelay2 (we don’t need to start a new workflow this time) and then startup the host.  The WorkflowHostDelay2 host will sit there indefinitely and not reload the workflow it originally started.   But wait, why would this host have to reload the same workflow that it had originally started?

The answer is, it doesn’t.  Steve Danielson (Microsoft - https://blogs.msdn.com/sdanie/) pointed out a few things.

What’s going on here is that when HostWithOnlyaRuntime starts, the SQLWorkflowPersistenceService puts a lock on these workflows because it wants to load them, but it can’t because this runtime has no reference to the workflow types WorkflowDelay1.WFDelay1 and WorkflowDelay2.WFDelay2.  To allow HostWithOnlyaRuntime to reload these workflows, all I would have to do is add a project reference to the WorkflowDelay1 and WorkflowDelay2 projects.

image

In my customers case, he had two different application servers but on one of the machines, he did not have a reference to one of the workflow types.  On this application server that was missing the workflow type reference, when the host would start, it would leave expired workflows in the database.

I hope this helps someone who might be facing a similar scenario.