1
votes

I am currently experimenting with Workflow Foundation 4.5 in a client, WCF server architecture.

The way i set up the server is by creating a regular WCF Service Application (not a WCF Workflow Service Application) and added a bunch of web methods to start, resume, get states of of entities living in the workflow.

So i have things like :

public class WebService1 : WebService
{
    private const string ConnectionString = "Server=.\\SQLEXPRESS;Initial Catalog=WF45GettingStartedTutorial;Integrated Security=SSPI";
    private SqlWorkflowInstanceStore _store;

    public SqlWorkflowInstanceStore Store
    {
        get
        {
            if (_store == null)
            {
                Debug.WriteLine("Building new store");
                _store = new SqlWorkflowInstanceStore(ConnectionString);
                StateMachineStateTracker.Promote(Store);
            }

            return _store;
        }
    }

    [WebMethod]
    public Guid Start(Guid resourceId)
    {
        var activity = new Activity1(); // My state machine
        var parameters = new Dictionary<string, object> { { "resourceId", resourceId } };
        var wfApp = new WorkflowApplication(activity, parameters, new WorkflowIdentity("v1", new Version(1, 0), string.Empty));
        ConfigureWorkflowApplication(wfApp);
        wfApp.Run();
        return wfApp.Id;
    }

    [WebMethod]
    public void Resume(Guid instanceId, string bookmarkName, object bookmarkParameter)
    {
        var activity = new Activity1();
        var wfApp = new WorkflowApplication(activity, new WorkflowIdentity("v1", new Version(1, 0), string.Empty));
        ConfigureWorkflowApplication(wfApp);
        wfApp.Load(instanceId);
        wfApp.ResumeBookmark(bookmarkName, bookmarkParameter);
    }

    [WebMethod]
    public string GetCurrentState(Guid instanceId)
    {
        var activity = new Activity1();

        // Things get messy here :

        // var instance = WorkflowApplication.GetInstance(instanceId, Store);
        // var wfApp = new WorkflowApplication(activity, instance.DefinitionIdentity);

        // so replaced with :
        var wfApp = new WorkflowApplication(activity, new WorkflowIdentity("v1", new Version(1, 0), string.Empty));

        ConfigureWorkflowApplication(wfApp);
        var trackerInstance = StateMachineStateTracker.LoadInstance(instanceId, wfApp.WorkflowDefinition, ConnectionString);
        if (trackerInstance != null)
        {
            return trackerInstance.CurrentState;
        }

        return string.Empty;
    }

    private void ConfigureWorkflowApplication(WorkflowApplication wfApp)
    {
        // Configure the persistence store.
        wfApp.InstanceStore = Store;

        // State machine tracker taken from http://code.msdn.microsoft.com/windowsdesktop/Windows-Workflow-fee72008
        var tracker = new StateMachineStateTracker(wfApp.WorkflowDefinition);
        wfApp.Extensions.Add(tracker);
        wfApp.Extensions.Add(new StateTrackerPersistenceProvider(tracker)); 

        wfApp.Completed = delegate(WorkflowApplicationCompletedEventArgs e)
        {
            Debug.WriteLine("Workflow completed.");
        };

        wfApp.Aborted = delegate(WorkflowApplicationAbortedEventArgs e)
        {
            Debug.WriteLine(string.Format("Workflow Aborted. Exception: {0}\r\n{1}",
                    e.Reason.GetType().FullName,
                    e.Reason.Message));
        };

        wfApp.OnUnhandledException = delegate(WorkflowApplicationUnhandledExceptionEventArgs e)
            {
                Debug.WriteLine(string.Format("Unhandled Exception: {0}\r\n{1}",
                                              e.UnhandledException.GetType().FullName,
                                              e.UnhandledException.Message));
            return UnhandledExceptionAction.Terminate;
        };

        wfApp.PersistableIdle = delegate(WorkflowApplicationIdleEventArgs e)
        {
            return PersistableIdleAction.Unload;
        };
    }
}

Now, what i want is to get the workflow version from the instance id, so in my GetCurrentState i call :

var instance = WorkflowApplication.GetInstance(instanceId, Store);

And load the WorkflowApplication with that

var wfApp = new WorkflowApplication(activity, instance.DefinitionIdentity)

this works fine, but i suspect somehow leaves the host of my workflow loaded, and my instance locked.

So when right after calling GetCurrentState(), the client calls Resume() i get the following exception (at the wfApp.Load(instanceId) line).

InstanceLockedException

The execution of an InstancePersistenceCommand was interrupted because the instance 'a1bcbd11-50fc-4a72-b5d2-87b71d0c3c45' is locked by a different instance owner. This error usually occurs because a different host has the instance loaded. The instance owner ID of the owner or host with a lock on the instance is '190ea0d9-788f-4278-883e-84226f5788bc'.

This exception goes away when i stop using the WorkflowApplication.GetInstance method and specifying the version manually, but i would rather avoid that.

So what am i doing wrong here ?

Googling around took me to these pages :

InstanceLockedException: How to handle locking issues with WF 4.0?

-> Looks like the issue i have, but i am not sure how where i can set this timeToUnload value in my current code as i do not use WorkflowServiceHost. Should i just create a new one in each webmethod ? or make a singleton ?

http://social.msdn.microsoft.com/Forums/en-US/3e38a60e-8a99-4c01-a26b-f82670ca5601/instancelockedexception-when-application-restarts-using-persistableidleactionpersist?forum=wfprerelease

-> Tried this on my store with a TimeSpan of 1 sec, did not help either.

Any suggestions would be greatly appreciated :-)

FOLLOW UP

After reading this post : Error when attempting to Resume a Windows Workflow

I have created a DisposableStore class designed as follow :

public class DisposableStore : IDisposable
{
    private const string ConnectionString = "Server=.\\SQLEXPRESS;Initial Catalog=WF45GettingStartedTutorial;Integrated Security=SSPI";
    private SqlWorkflowInstanceStore _store;
    private InstanceHandle _handle;

    public SqlWorkflowInstanceStore Store
    {
        get
        {
            if (_store == null)
            {
                Debug.WriteLine("Building new store");
                _store = new SqlWorkflowInstanceStore(ConnectionString);
                StateMachineStateTracker.Promote(_store);

                _handle = _store.CreateInstanceHandle();
                InstanceView view = _store.Execute(_handle, new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
                _store.DefaultInstanceOwner = view.InstanceOwner;
            }

            return _store;
        }
    }

    public void Dispose()
    {
        var deleteOwnerCmd = new DeleteWorkflowOwnerCommand();
        Store.Execute(_handle, deleteOwnerCmd, TimeSpan.FromSeconds(30));
    }
}

And i use it as follow :

[WebMethod]
public bool Resume(Guid instanceId, string bookmarkName, object bookmarkParameter)
{
    Debug.WriteLine(string.Format("Resume - Service id : {0}", _serviceId));
    WorkflowDescriptor descriptor;
    using (var store = new DisposableStore())
    {
        var instance = WorkflowApplication.GetInstance(instanceId, store.Store);
        descriptor = WorkflowLocator.GetWorflowFromIdentity(instance.DefinitionIdentity);
    }

    using (var store = new DisposableStore())
    {
        var wfApp = new WorkflowApplication(descriptor.Activity, descriptor.Identity);
        ConfigureWorkflowApplication(wfApp, store.Store);
        wfApp.Load(instanceId);
        var sync = new AutoResetEvent(false);
        wfApp.Idle = x => sync.Set();
        wfApp.Completed = x=> sync.Set();
        wfApp.ResumeBookmark(bookmarkName, bookmarkParameter);
        sync.WaitOne();
     }

     return true;
}

Things get better with this approach, meaning i no longer get the InstanceLockedException, but now when the Resume method returns to the client, i my wfApp gets aborted :

Exception: System.Runtime.DurableInstancing.InstanceOwnerException The execution of an InstancePersistenceCommand was interrupted because the instance owner registration for owner ID '33ed6492-0685-4f31-8652-4b91acaf50ef' has become invalid. This error indicates that the in-memory copy of all instances locked by this owner have become stale and should be discarded, along with the InstanceHandles. Typically, this error is best handled by restarting the host

1

1 Answers

4
votes

Ok, so i was on the right track using a DisposableStore. The only issue what that the store owner was deleted too soon because of this line :

wfApp.Idle = x => sync.Set();

This was causing my synchroniser to be released too soon, causing the dispose method (which kills the store) to be called before the workflow had a chance to persist itself (which requires the store & its owner).

So i replaced this line with :

wfApp.Unloaded = x => sync.Set();

This way, i wait for the workflow instance to be unloaded before releasing my synchroniser.

So for people facing similar issues, here is the big picture the final code :

[WebMethod]
public bool Resume(Guid instanceId, string bookmarkName, object bookmarkParameter)
{
    Debug.WriteLine(string.Format("Resume - Service id : {0}", _serviceId));
    WorkflowDescriptor descriptor;
    using (var store = new DisposableStore())
    {
        var instance = WorkflowApplication.GetInstance(instanceId, store.Store);
        descriptor = WorkflowLocator.GetWorflowFromIdentity(instance.DefinitionIdentity);
    }

    // We have to create a new store at this point, can't use the same one !!!

    using (var store = new DisposableStore())
    {
        var wfApp = new WorkflowApplication(descriptor.Activity, descriptor.Identity);
        ConfigureWorkflowApplication(wfApp, store.Store);
        wfApp.Load(instanceId);
        var sync = new AutoResetEvent(false);
        wfApp.ResumeBookmark(bookmarkName, bookmarkParameter);
        wfApp.Unloaded = x => sync.Set();
        sync.WaitOne();
    }

    return true;
}