1
votes

I have an application that executes workflows using WorkflowApplication as well as doing other things. I would like both these other things and the WorkflowApplication interactions with the instance store prior to calling BeginRun() to be scoped by a transaction so I've wrapped everything in one. However, introducing this transaction causes calls such as WorkflowApplication.Persist() to hang. The following example is a modified version of the basic instance persistence sample (described here, download from here). I modified the original by scoping everything done in a transaction; this mimics very closely what my application does.

using System;
using System.Activities;
using System.Activities.DurableInstancing;
using System.Activities.Statements;
using System.Runtime.DurableInstancing;
using System.Threading;

namespace Microsoft.Samples.Activities
{
  using System.Transactions;

  class Program
  {
    static AutoResetEvent instanceUnloaded = new AutoResetEvent(false);
    static Activity activity = CreateWorkflow();
    static Guid id;

    const string readLineBookmark = "ReadLine1";

    static void Main()
    {
      StartAndUnloadInstance();
      LoadAndCompleteInstance();

      Console.WriteLine("Press [Enter] to exit.");
      Console.ReadLine();
    }

    static void StartAndUnloadInstance()
    {
      using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew))
      {
        WorkflowApplication application = new WorkflowApplication(activity);
        InstanceHandle handle;
        application.InstanceStore = SetupInstanceStore(out handle);

        application.Completed = (e) =>
          { handle.Free(); };

        application.PersistableIdle = (e) =>
            {
              return PersistableIdleAction.Unload;
            };

        application.Unloaded = (e) =>
            {
              instanceUnloaded.Set();
            };

        application.Persist();
        id = application.Id;
        application.Run();
        ts.Complete();
      }

      instanceUnloaded.WaitOne();
    }

    static void LoadAndCompleteInstance()
    {
      using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew))
      {
        string input = Console.ReadLine();

        WorkflowApplication application = new WorkflowApplication(activity);
        InstanceHandle handle;
        application.InstanceStore = SetupInstanceStore(out handle);

        application.Completed = (workflowApplicationCompletedEventArgs) =>
          {
            handle.Free();
            Console.WriteLine(
              "\nWorkflowApplication has Completed in the {0} state.",
              workflowApplicationCompletedEventArgs.CompletionState);
          };

        application.Unloaded = (workflowApplicationEventArgs) =>
          {
            Console.WriteLine("WorkflowApplication has Unloaded\n");
            instanceUnloaded.Set();
          };

        application.Load(id);

        //this resumes the bookmark setup by readline
        application.ResumeBookmark(readLineBookmark, input);
        ts.Complete();
      }

      instanceUnloaded.WaitOne();
    }

    static Sequence CreateWorkflow()
    {
      Variable<string> response = new Variable<string>();

      return new Sequence()
      {
        Variables = { response },
        Activities = { 
                        new WriteLine(){
                            Text = new InArgument<string>("What is your name?")},
                        new ReadLine(){ 
                            BookmarkName = readLineBookmark, 
                            Result = new OutArgument<string>(response)},
                        new WriteLine(){
                            Text = new InArgument<string>((context) => "Hello " + response.Get(context))}}
      };
    }

    private static InstanceStore SetupInstanceStore(out InstanceHandle handle)
    {
      SqlWorkflowInstanceStore instanceStore =
          new SqlWorkflowInstanceStore(@"Data Source=.;Initial Catalog=SampleInstanceStore;Integrated Security=True;Asynchronous Processing=True");

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

      instanceStore.DefaultInstanceOwner = view.InstanceOwner;

      return instanceStore;
    }
  }
}

using System;
using System.Activities;
using System.Activities.DurableInstancing;
using System.Activities.Statements;
using System.Runtime.DurableInstancing;
using System.Threading;

namespace Microsoft.Samples.Activities
{
  using System.Transactions;

  class Program
  {
    static AutoResetEvent instanceUnloaded = new AutoResetEvent(false);
    static Activity activity = CreateWorkflow();
    static Guid id;

    const string readLineBookmark = "ReadLine1";

    static void Main()
    {
      StartAndUnloadInstance();
      LoadAndCompleteInstance();

      Console.WriteLine("Press [Enter] to exit.");
      Console.ReadLine();
    }

    static void StartAndUnloadInstance()
    {
      using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew))
      {
        WorkflowApplication application = new WorkflowApplication(activity);
        InstanceHandle handle;
        application.InstanceStore = SetupInstanceStore(out handle);

        application.Completed = (e) =>
          { handle.Free(); };

        application.PersistableIdle = (e) =>
            {
              return PersistableIdleAction.Unload;
            };

        application.Unloaded = (e) =>
            {
              instanceUnloaded.Set();
            };

        application.Persist();
        id = application.Id;
        application.Run();
        ts.Complete();
      }

      instanceUnloaded.WaitOne();
    }

    static void LoadAndCompleteInstance()
    {
      using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew))
      {
        string input = Console.ReadLine();

        WorkflowApplication application = new WorkflowApplication(activity);
        InstanceHandle handle;
        application.InstanceStore = SetupInstanceStore(out handle);

        application.Completed = (workflowApplicationCompletedEventArgs) =>
          {
            handle.Free();
            Console.WriteLine(
              "\nWorkflowApplication has Completed in the {0} state.",
              workflowApplicationCompletedEventArgs.CompletionState);
          };

        application.Unloaded = (workflowApplicationEventArgs) =>
          {
            Console.WriteLine("WorkflowApplication has Unloaded\n");
            instanceUnloaded.Set();
          };

        application.Load(id);

        //this resumes the bookmark setup by readline
        application.ResumeBookmark(readLineBookmark, input);
        ts.Complete();
      }

      instanceUnloaded.WaitOne();
    }

    static Sequence CreateWorkflow()
    {
      Variable<string> response = new Variable<string>();

      return new Sequence()
      {
        Variables = { response },
        Activities = { 
                        new WriteLine(){
                            Text = new InArgument<string>("What is your name?")},
                        new ReadLine(){ 
                            BookmarkName = readLineBookmark, 
                            Result = new OutArgument<string>(response)},
                        new WriteLine(){
                            Text = new InArgument<string>((context) => "Hello " + response.Get(context))}}
      };
    }

    private static InstanceStore SetupInstanceStore(out InstanceHandle handle)
    {
      SqlWorkflowInstanceStore instanceStore =
          new SqlWorkflowInstanceStore(@"Data Source=.;Initial Catalog=SampleInstanceStore;Integrated Security=True;Asynchronous Processing=True");

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

      instanceStore.DefaultInstanceOwner = view.InstanceOwner;

      return instanceStore;
    }
  }
}

When run, the application hangs on the Persist() call in StartAndUnloadInstance(). The hang ends after about five minutes (there's a timeout somewhere). While hung, you can see the reason why by looking at the Activity Monitor in SQL Server. The call to create the workflow owner in SetupInstanceStore() acquires an exclusive lock to a row in the LockOwnersTable. The call to Persist() attempts acquire the same lock. Whoops.

Anyway, the broader question is as stated in the title: can the instance store interactions I do before calling BeginRun() work within the context of the ambient transaction or not? If so, what am I doing wrong? If not, what options do I have? Thus far, I have been able to eliminate the hang/deadlock by surrounding each case where WorkflowApplication accesses the instance store with:

using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Suppress))
{
  // Method call that interacts with the instance store.
  ts.Complete();
}

This moves me forward slightly but reduces me to writing compensation code to handle these calls failing. This should not be necessary. Insights would be greatly appreciated.

EDIT: To elaborate further, I want the workflow to be persisted before execution within the same transaction as other database activity. I want this to:

  • Allow for the workflow to be re-run if the host process dies during the execution of the workflow.
  • Ensure that persistence is rolled back and the workflow is never run if the other database activity fails.

The workflow itself does not need to be run within a transaction, only the instance store interactions that occur prior to the call to BeginRun().

3

3 Answers

2
votes

Alright, so the answer to my question appears to be no. I can't find a way to eliminate the hang while the ambient transaction is present. What I've done is surround each instance store interaction (create workflow owner, persist) with

using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Suppress))
{
  // Method call that interacts with the instance store.
  ts.Complete();
}

This allows this structure for kicking off workflows:

  • Start top-level unit of work (NHibernate session).
    • Do stuff.
    • Get the instance store handle, execute a CreateWorkflowOwnerCommand to acquire the lock.
    • Create the workflow application.
    • Persist the workflow.
    • Call BeginRun.
  • Commit.

The downside? If an exception is thrown after the Persist, there's no rollback; the workflow will run at some point. I have some infrastructure in place to minimize the problem but the long-term answer is to find a way to use AppFabric (not possible at this point due to scale of change required but will be looked at for the next version).

0
votes

a workflow can run within a transaction but doing so using the WorkflowApplication takes more work. For one the workflow actually executes on a background thread and not on the one where you created the TransactionScope. If you use a WorkflowInvoker it will run as part of the same transaction on the same thread.

Another thing to keep in mind is the time a workflow takes to run. Depends on your workflow of course but often workflows run over a longer time period and keeping an transaction active for along time is not a good plan.

Inside a workflow you can use the TranscationScopeActivity. Basically an activity that wraps a number of child activities in a TransactionScope. It doesn't expose everything a regular TransactionScope does but is quite useful.

But with long running workflows a far better model is to use the CompensableActivity and compensatable transactions.

0
votes

WorkflowApplication by default runs the workflow on a different thread from the host thread. The Ambient transaction is accessible only by code that is running on the host thread. You cannot share the Ambient transaction across threads.

You could get WorkflowApplication to use the calling thread by setting the SynchronizationContext. But a better solution is to let each workflow instance manage it's own transactions. If there is a transaction scope, persistence will occur under the transaction. Workflow always persists at transaction boundaries.