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().