OdeToCode IC Logo

Managing Windows Workflow Events on a Web Server

Sunday, January 21, 2007

Once you have the WorkflowRuntime up and running in an ASP.NET application or web service, you'll want handle key life cycle events like WorkflowTerminated and WorkflowCompleted. I want to warn you about some common pitfalls I've seen.

There are two subtle but extremely dangerous problems in the following code.

protected void Page_Load(object sender, EventArgs e)
{
    
// get a reference to our WorkflowRuntime singleton
    WorkflowRuntime runtime = ApplicationInstance.WorkflowRuntime;
    
    
// wire up an event handler
    runtime.WorkflowCompleted +=
        
new EventHandler<WorkflowCompletedEventArgs>
              (runtime_WorkflowCompleted);

    
// create and start a new workflow
    WorkflowInstance workflow;
    workflow = runtime.CreateWorkflow(
typeof(SomeWorkflow));
    workflow.Start();

    
// ask the manual scheduling service to execute the
    // workflow on this very thread
    ManualWorkflowSchedulerService scheduler;
    scheduler = runtime.GetService<
ManualWorkflowSchedulerService>();
    scheduler.RunWorkflow(workflow.InstanceId);
}

void runtime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e)
{
    
// depending on what the workflow did for us, this is where we would
    // tell the user the final price, or approve their document, or whatever
    // workflowy thing that might have happened
}

Think: Singletons and event handling. The WorkflowRuntime will typically be a singleton in an ASP.NET app. In the WF RTM version, you can have multiple WF runtimes executing inside the same AppDomain (this wasn’t true in the betas), but chances are you don't want to pay the performance overhead of spinning up a new WF runtime for each request. Thus, we have a single and globally accessible WF runtime. For questions about the ManualWorkflowSchedulerService, see my article on hosting Windows Workflow. This scheduler allows us to run the workflow instance synchronously - on the same thread as the request.

Problem #1: Someone Just Expensed a Ferrari

The WorkflowCompleted event will fire for any workflow that completes, not just the workflow we are executing inside this page (or web request).

Let's say the workflow is running some business rules to automatically approve or reject expense reports. This usually requires a human bean counter to get involved, but stick with me for a second and pretend it's all done in silicon. Let's say the workflow indicates approval or rejection with the OutputParameters property of the WorkflowCompletedEventArgs.

User Joe submits an expense report for his new red stapler. At the same time, I submit an expense report for a red Ferrari Enzo. There are two pages executing on the server, and both subscribed to the WorkflowCompleted event. If things work out just right, Joe's workflow will complete first and approve his expense report. The runtime will raise the completed event, which both pages handle. Since Joe's workflow indicated approval, Joe will soon be stapling papers while I get to go 0-60 in less than 3.5 seconds.

Problem #2: We Need More Power, Scotty!

To fire the completed event, the WorkflowRuntime maintains a reference to the Page object. We should assume the WorkflowRuntime, as a singleton, will live for the duration of the AppDomain. Assuming the Page object never un-subscribes from the completed event, it will be in memory for the duration of the AppDomain, too. The garbage collector can't take the Page out of memory while it's being referenced by the WF runtime. RAM will start to disappear over time.

Even worse, with each request another instance of this Page class will wire itself to the WorkflowRuntime, and each time a workflow completes the WF runtime will need to raise the event to all these zombie page objects. This will keep the CPU busy, too.

It won't be long before the server is sucking mud, and the company is out of money.