| | | 1 | | using Elsa.Extensions; |
| | | 2 | | using Elsa.Workflows; |
| | | 3 | | |
| | | 4 | | namespace Elsa.Bpmn.Hosting; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Stops a unit of work and everything it in turn started. |
| | | 8 | | /// </summary> |
| | | 9 | | /// <remarks> |
| | | 10 | | /// Both places that tear work down go through here: the interpreter's <c>CancelWorkSubtree</c> command, and a scope |
| | | 11 | | /// terminalizing the unit of work whose fault it just claimed. The mechanism is the same in either case, and so is the |
| | | 12 | | /// one thing this host cannot do. |
| | | 13 | | /// </remarks> |
| | | 14 | | internal static class BpmnWorkTeardown |
| | | 15 | | { |
| | | 16 | | /// <summary>The activity execution with the given id, or <c>null</c> when it has already gone.</summary> |
| | | 17 | | public static ActivityExecutionContext? FindContext(WorkflowExecutionContext workflowExecutionContext, string activi |
| | 25 | 18 | | workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => string.Equals(x.Id, activityExecutionCont |
| | | 19 | | |
| | | 20 | | /// <summary> |
| | | 21 | | /// Tears down a unit of work and everything underneath it, or refuses when it cannot. |
| | | 22 | | /// </summary> |
| | | 23 | | /// <remarks> |
| | | 24 | | /// <para> |
| | | 25 | | /// Cancellation itself needs no new code: the public <c>CancelActivityAsync</c> extension already walks the child |
| | | 26 | | /// subtree recursively, which is exactly what "and everything it in turn started" means. |
| | | 27 | | /// </para> |
| | | 28 | | /// <para> |
| | | 29 | | /// What it cannot do is withdraw work whose activity is scheduled but has not been invoked yet. Such work has no |
| | | 30 | | /// running execution to cancel, and <c>IActivityScheduler</c> offers no way to remove a queued work item, so the |
| | | 31 | | /// activity runs after BPMN destroyed the branch it belongs to regardless of what this method does. Throwing |
| | | 32 | | /// still fails loudly under <c>FaultStrategy</c>, which is the strategy that justifies it. It does not fail |
| | | 33 | | /// loudly under <c>ContinueWithIncidentsStrategy</c>: there the throw is absorbed into an incident, execution |
| | | 34 | | /// continues, and the stranded activity still runs. What limits the damage in that case is the caller's own |
| | | 35 | | /// doing, not this method's: the caller removes the work's ledger record and persists that removal before |
| | | 36 | | /// invoking this method, so an absorbed throw cannot leave the persisted ledger claiming work that was just torn |
| | | 37 | | /// down, and the stranded activity's eventual completion callback finds no live record and is discarded rather |
| | | 38 | | /// than handed to the interpreter as real work. That does not stop the stray activity from running, and it does |
| | | 39 | | /// not undo whatever side effects it has — it only stops its result from being believed. |
| | | 40 | | /// </para> |
| | | 41 | | /// </remarks> |
| | | 42 | | public static async ValueTask CancelSubtreeAsync(ActivityExecutionContext childContext, string reason) |
| | | 43 | | { |
| | 9 | 44 | | var scheduler = childContext.WorkflowExecutionContext.Scheduler; |
| | 9 | 45 | | var subtree = new[] { childContext }.Concat(childContext.GetDescendants()); |
| | 31 | 46 | | var queued = subtree.FirstOrDefault(context => scheduler.Any(item => item.ExistingActivityExecutionContext?.Id = |
| | | 47 | | |
| | 9 | 48 | | if (queued is not null) |
| | | 49 | | { |
| | 3 | 50 | | throw new NotSupportedException( |
| | 3 | 51 | | $"BPMN asked to tear down the work of activity '{childContext.Activity.Id}' ({reason}), but activity " |
| | 3 | 52 | | + $"'{queued.Activity.Id}' inside that subtree is scheduled and has not started, and a scheduled work " |
| | 3 | 53 | | + "item cannot be withdrawn. Running it anyway would leave a branch alive that BPMN destroyed."); |
| | | 54 | | } |
| | | 55 | | |
| | 6 | 56 | | await childContext.CancelActivityAsync(); |
| | 6 | 57 | | } |
| | | 58 | | } |