// This program runs multiple threads: // RunnableInstancesDetectionThread // This thread periodically polls the database and waits for workflow instances // in the "runnable" state. When that happens, it raises an event and pauses // until runnable instances have been processed (by another thread). // // ResumptionBookmarkThread // This is the user interface thread using console input for a user to type commands. // It implements a REPL (read-eval-print-loop) environment where a user can issue // bookmark resumption commands. When this happens, the thread raises an event // for bookmarks to be processed (by another thread). // Valid commands have the following format: // <app-name>[ <instance-id>] // // ProcessingThread // This thread waits for events raised from other sources. // Valid events are: // - HasRunnableInstances // - HasBookmark // // When "HasBookmark" is raised, the thread resumes the // corresponding in-memory workflow instance. // // When "HasRunnableInstance" is raised, the thread attempts // to call the "LoadRunnableInstance" method to load the // corresponding workflow instance from the database. // This must be done until the "LoadRunnableInstance" method // raises and "InstanceNotReadyException". This signals that // all runnable instances from the database have been processed. // At this stage, the thread raises an event to resume the // RunnableInstancesDetectionThread. // CHALLENGES: // // In order to be able to resume a persisted runnable workflow instance // it is necessary to know the workflow identity in order to match it // with the corresponding XAML definition. This sample associates a // unique identity for each XAML definition that is automatically // persisted to the database and can be retrieved on the "HasRunnableInstance" event. // // This sample persists and unloads workflow instances when bookmarked. // Therefore, upon resumption, the a new instance is created with the corresponding // XAML definition and the workflow execution is resumed. // In crosscut, we try to prevent workflow instances from being unloaded if another // message is received in a short period of time. Try to prevent workflow instances // from being unloaded with this sample first. static void Main(string[] args) { var cmdLine = CommandLine.Parse(args); var crash = cmdLine.Crash; var operation = cmdLine.Operation; string bookmarkName = null; string instanceId = null; var hasRunnableInstances = new ManualResetEvent(false); var stopping = new ManualResetEvent(false); var monitorRunnableInstances = new AutoResetEvent(false); var hasBookmark = new AutoResetEvent(false); var store = CreateInstanceStore(out var handle); try { // RunnableInstancesDetectionThread // This thread periodically polls the database and waits for workflow instances // in the "runnable" state. When that happens, it raises an event and pauses // until runnable instances have been processed (by another thread). // var runnableInstancesDetectionThread = new Thread(unused => { while (true) { var status = WaitHandle.WaitAny(new WaitHandle[] { stopping, monitorRunnableInstances, }); if (status == 0) { break; } if (status == 1) { var succeeded = WaitForRunnableInstance(store, handle, TimeSpan.FromSeconds(15.0)); if (succeeded) { hasRunnableInstances.Set(); } else { monitorRunnableInstances.Set(); } } } }); // ResumptionBookmarkThread // This is the user interface thread using console input for a user to type commands. // It implements a REPL (read-eval-print-loop) environment where a user can issue // bookmark resumption commands. When this happens, the thread raises an event // for bookmarks to be processed (by another thread). // Valid commands have the following format: // <app-name>[ <instance-id>] // var resumptionBookmarkThread = new Thread(unused => { while (true) { Console.WriteLine(); Console.WriteLine("Please, press ENTER to exit or type a bookmark name and ENTER to resume the specified bookmark."); var line = Console.ReadLine(); if (line == "") { stopping.Set(); break; } else { var match = BookmarkRegex.Match(line); if (match.Success) { if (match.Groups["instance"] != null) { instanceId = match.Groups["instance"].Value; } bookmarkName = match.Groups["app"].Value; hasBookmark.Set(); } else { Console.Error.WriteLine("Invalid syntax."); } } } }); runnableInstancesDetectionThread.IsBackground = true; runnableInstancesDetectionThread.Start(); resumptionBookmarkThread.Start(); monitorRunnableInstances.Set(); var instances = new Guid[2] { Guid.Empty, Guid.Empty, }; // The application starts a instance for two different workflow types // Instances are kept in memory for the duration of their lifetime. if (operation == CommandLine.Operations.Run) { Console.WriteLine("Running instances of two difference workflow apps..."); instances[0] = RunWorkflow(store, WorkflowApps.App1); instances[1] = RunWorkflow(store, WorkflowApps.App2); } // ProcessingThread // This thread waits for events raised from other sources. // Valid events are: // - HasRunnableInstances // - HasBookmark // // When "HasBookmark" is raised, the thread resumes the // corresponding in-memory workflow instance. // // When "HasRunnableInstance" is raised, the thread attempts // to call the "LoadRunnableInstance" method to load the // corresponding workflow instance from the database. // This must be done until the "LoadRunnableInstance" method // raises and "InstanceNotReadyException". This signals that // all runnable instances from the database have been processed. // At this stage, the thread raises an event to resume the // RunnableInstancesDetectionThread. while (true) { var status = WaitHandle.WaitAny(new WaitHandle[] { stopping, hasRunnableInstances, hasBookmark, }); if (status == WaitHandle.WaitTimeout) { throw new TimeoutException(); } // stopping if (status == 0) { break; } // hasRunnableInstances if (status == 1) { try { var instance = WorkflowApplication.GetRunnableInstance(store); if (crash) { throw new ApplicationException("CRASHED"); } LoadRunnableInstance(store, instance); } catch (InstanceNotReadyException) { // no more runnable instances Console.Error.WriteLine("Resuming runnable instances detection."); hasRunnableInstances.Reset(); monitorRunnableInstances.Set(); } } // hasBookmark if (status == 2) { System.Diagnostics.Debug.Assert(!String.IsNullOrEmpty(bookmarkName)); Console.WriteLine($"Resuming workflow with bookmark: \"{bookmarkName}\"."); var identifier = Guid.Empty; if (String.IsNullOrEmpty(instanceId)) { var index = 0; if (bookmarkName == "App2") { index = 1; } identifier = instances[index]; } else { identifier = Guid.Parse(instanceId); } ResumeWorkflow(store, identifier, bookmarkName); } } Console.WriteLine("Done."); resumptionBookmarkThread.Join(); runnableInstancesDetectionThread.Join(); } finally { } }