Tuesday, 20 January 2015

Self hosting workflow–working with InstanceStore and InstanceOwner

I’ve not been doing a great deal of Workflow recently but had to fix an issue at a customer today and as there’s such poor information about this on the Web I put fingers to keys to blog about it.

If you are self-hosting workflows using WorkflowApplication you’ll typically have some code such as the following to setup the workflow instance store…

    instanceStore = new SqlWorkflowInstanceStore
(@"Data Source=.\SQLEXPRESS;Initial Catalog=SampleInstanceStore;
Integrated Security=True;Asynchronous Processing=True");

InstanceHandle handle = instanceStore.CreateInstanceHandle();
InstanceView view = instanceStore.Execute(handle,
new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
handle.Free();

instanceStore.DefaultInstanceOwner = view.InstanceOwner;

The question is - where does this go? I’ll get on to that in a moment.


When you want to run a workflow you need to set it’s instance store – so you’ll typically do this…

    WorkflowApplication application = new WorkflowApplication(activity);
application.InstanceStore = instanceStore;

And then somewhere online you might have seen the DeleteWorkflowOwnerCommand as that’s the opposite of the CreateWorkflowOwnerCommand so has to be called somewhere, doesn’t it?


If all of this has you confused this is the article that will help you.


Instance Store: One Per Process


When creating the instance store you need no more than one per process. You can run multiple workflow instances in this process, and each will play nicely together – but all will use the same store. You don’t need loads, one will do per process.


I’m using AutoFac so have setup a dependency to be SingleInstance(), and each workflow wrapper that I run imports that single instance. The wrapper is shown below…

    public interface IWorkflowInstanceStore
{
InstanceStore Store { get; }
}

The implementation is also pretty trivial...

    public class WorkflowInstanceStore : IWorkflowInstanceStore, IDisposable
{
public WorkflowInstanceStore(string connectionString)
{
_instanceStore = new SqlWorkflowInstanceStore(connectionString);

InstanceHandle handle = _instanceStore.CreateInstanceHandle();
InstanceView view = _instanceStore.Execute(handle,
new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
handle.Free();

_instanceStore.DefaultInstanceOwner = view.InstanceOwner;
}

public InstanceStore Store
{
get { return _instanceStore; }
}

public void Dispose()
{
if (null != _instanceStore)
{
var deleteOwner = new DeleteWorkflowOwnerCommand();
InstanceHandle handle = _instanceStore.CreateInstanceHandle();
_instanceStore.Execute(handle, deleteOwner, TimeSpan.FromSeconds(10));
handle.Free();
}
}

private InstanceStore _instanceStore;
}

As you'll see here this is a disposable class and in the Dispose() method I call the DeleteWorkflowOwnerCommand. When running workflows, you'll get a row in the LockOwners for each instance store, and these are tidied up by the RecoverInstanceLocks stored procedure which runs periodically. This can be a source of poor performance, as if you use an instance store per workflow application (which is very common given the lack of guidance on this matter), then you'll end up with a large number of rows in this table and RecoverInstanceLocks will show up on your SQL traces as taking a long time to execute. The culprit isn't directly the stored procedure, it's the use of too many rows in the LockOwnersTable. Typically there should be just one row in there per process that runs workflows.


Workflow Runner: One per workflow application


Next up you’ll need a class that runs workflows – typically you’ll write a simple wrapper such as the one I’ve done below, this wraps running workflows and resuming bookmarks with Task based operations so that you can easily integrate Workflow into your code. Note – you might want to process other events and probably do some logging inside this class too, I’ve just provided a minimal implementation…

    public class WorkflowHoster
{
public WorkflowHoster(IWorkflowInstanceStore instanceStore,
params object[] services)
{
_instanceStore = instanceStore.Store;
_services = services.ToList();
}

public Task RunInstance(Activity root)
{
WorkflowApplication application = new WorkflowApplication(root);
foreach (var service in _services)
application.Extensions.Add(service);
var tcs = new TaskCompletionSource();

application.InstanceStore = _instanceStore;

application.PersistableIdle = (e) => PersistableIdleAction.Unload;
application.Unloaded = (e) => tcs.SetResult(e.InstanceId);
application.Aborted = (e) => tcs.SetException(e.Reason);
application.Run();

return tcs.Task;
}

public Task ResumeInstance(Activity root, Guid workflowInstanceId,
string bookmarkName, object bookmarkData = null)
{
WorkflowApplication application = new WorkflowApplication(root);
application.InstanceStore = _instanceStore;
var tcs = new TaskCompletionSource();

application.PersistableIdle = (e) => PersistableIdleAction.Unload;
application.Unloaded = (e) => tcs.SetResult(DateTime.UtcNow);
application.Aborted = (e) => tcs.SetException(e.Reason);
application.Load(workflowInstanceId);
application.ResumeBookmark(bookmarkName, bookmarkData);

return tcs.Task;
}

private InstanceStore _instanceStore;
private AutoResetEvent _instanceUnloaded = new AutoResetEvent(false);
private List<object> _services;
}

You may also want to pass parameters to the workflow instance etc. - as I mentioned the above is a minimal implementation just to show how this works.


Running a workflow


With that stuff defined you can run a workflow as follows…

    var store = GetThePerProcessStoreFromSomewhere();

var host = new WorkflowHoster(store);
Task wf = host.RunInstance(rootActivity);
// Now wait for the wf task to complete...

The task will complete when the workflow unloads (which obviously happens if the workflow completes entirely, or if it goes idle and persists due to a bookmark or delay).


Wrap Up


If you want to self-host workflow, then using your own persistence store requires a bit of knowledge which is hard to come by on the intertubes.


Create a single instance-store per process, and only call DeleteWorkflowOwnerCommand once when your process is shutting down. That should be all you need.

2 comments:

Christopher Klein said...

I'm just getting up to speed on starting, stopping, and resuming long running and your article is a great reference, so thanks. I have one question though: for ResumeInstance how do I determine the workflowInstanceId to pass in? I'm assuming it is something I can capture in RunInstance?

Thanks!
Chris

joer said...

I think the recommendation of one store per process is wrong. if you want to wake up runnable instances, you need to pass a unique name when creating the InstanceHandle and set it as the default InstanceOwner. This ties your store to ONE workflow type (Application), so you need ONE store PER workflow application