| | | 1 | | using Elsa.Workflows.Pipelines.WorkflowExecution; |
| | | 2 | | |
| | | 3 | | namespace Elsa.Workflows.Runtime.Middleware.Workflows; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Registers an <see cref="ExecutionCycleHandle"/> in <see cref="IExecutionCycleRegistry"/> for the duration of a |
| | | 7 | | /// single workflow execution cycle — the slice of execution between the pipeline entry and the next persistence |
| | | 8 | | /// boundary. The drain orchestrator counts these handles and, on deadline breach, force-cancels each one and marks |
| | | 9 | | /// the underlying instance <see cref="WorkflowSubStatus.Interrupted"/>. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <remarks> |
| | | 12 | | /// <para> |
| | | 13 | | /// Ingress source attribution: when a dispatcher knows which <see cref="IIngressSource"/> initiated the call, it sets |
| | | 14 | | /// <see cref="WorkflowExecutionContext.TransientProperties"/>[<see cref="IngressSourceNameKey"/>] before invoking the p |
| | | 15 | | /// The middleware reads it and forwards the name to <see cref="IExecutionCycleRegistry.BeginCycle"/>, which uses it to |
| | | 16 | | /// FR-018 invariant violation (a source that reports <see cref="IngressSourceState.Paused"/> but initiates a cycle anyw |
| | | 17 | | /// </para> |
| | | 18 | | /// </remarks> |
| | 531 | 19 | | public class ExecutionCycleTrackingMiddleware(WorkflowMiddlewareDelegate next, IExecutionCycleRegistry cycleRegistry) : |
| | | 20 | | { |
| | | 21 | | /// <summary> |
| | | 22 | | /// <see cref="WorkflowExecutionContext.TransientProperties"/> key used to convey the originating |
| | | 23 | | /// <see cref="IIngressSource.Name"/> from the dispatcher into the pipeline. |
| | | 24 | | /// </summary> |
| | | 25 | | public const string IngressSourceNameKey = "Elsa.Workflows.Runtime.IngressSourceName"; |
| | | 26 | | |
| | | 27 | | /// <summary> |
| | | 28 | | /// <see cref="WorkflowExecutionContext.TransientProperties"/> key under which the active |
| | | 29 | | /// <see cref="ExecutionCycleHandle"/> is stored for the duration of the workflow execution. |
| | | 30 | | /// <see cref="Services.ExecutionCycleAwareCommitStateHandler"/> retrieves and disposes the handle AFTER the |
| | | 31 | | /// runner's terminal commit has persisted the workflow state, so the drain orchestrator's force-cancel path |
| | | 32 | | /// (which awaits <c>handle.Disposed</c>) sees the runner's commit land BEFORE its own |
| | | 33 | | /// <see cref="WorkflowSubStatus.Interrupted"/> write — eliminating the runner-clobber race. |
| | | 34 | | /// </summary> |
| | | 35 | | public const string ExecutionCycleHandleKey = "Elsa.Workflows.Runtime.ExecutionCycleHandle"; |
| | | 36 | | |
| | | 37 | | public override async ValueTask InvokeAsync(WorkflowExecutionContext context) |
| | | 38 | | { |
| | 557 | 39 | | var ingressSourceName = context.TransientProperties.TryGetValue(IngressSourceNameKey, out var raw) ? raw as stri |
| | | 40 | | |
| | | 41 | | // The cancelCallback bridges the ExecutionCycleHandle's cancellation to the workflow execution itself: when |
| | | 42 | | // the drain orchestrator calls handle.Cancel() on deadline breach, WorkflowExecutionContext.Cancel() runs |
| | | 43 | | // synchronously, which fires the registered cancellation callback inside the context (transitioning the |
| | | 44 | | // workflow to Cancelled and clearing its schedule). The runner stops scheduling new activities; the |
| | | 45 | | // orchestrator awaits handle.Disposed (set by the ExecutionCycleAwareCommitStateHandler decorator AFTER |
| | | 46 | | // commit) and then overwrites the sub-status with Interrupted. |
| | 557 | 47 | | var handle = cycleRegistry.BeginCycle( |
| | 557 | 48 | | context.Id, |
| | 557 | 49 | | ingressSourceName, |
| | 557 | 50 | | context.CancellationToken, |
| | 557 | 51 | | cancelCallback: context.Cancel); |
| | 557 | 52 | | context.TransientProperties[ExecutionCycleHandleKey] = handle; |
| | | 53 | | |
| | | 54 | | try |
| | | 55 | | { |
| | 557 | 56 | | await Next(context); |
| | 557 | 57 | | } |
| | 0 | 58 | | catch |
| | | 59 | | { |
| | | 60 | | // Exception path only: WorkflowRunner won't reach commitStateHandler.CommitAsync, so the |
| | | 61 | | // ExecutionCycleAwareCommitStateHandler decorator never runs and the handle would leak. On the success |
| | | 62 | | // path we deliberately DO NOT dispose here — the runner commits AFTER pipeline.ExecuteAsync returns |
| | | 63 | | // (WorkflowRunner.cs:235), and the decorator must dispose AFTER that commit lands. Disposing here on |
| | | 64 | | // success would reintroduce the runner-clobber race: the drain orchestrator's wait on handle.Disposed |
| | | 65 | | // would complete before state was persisted. |
| | 0 | 66 | | handle.Dispose(); |
| | 0 | 67 | | throw; |
| | | 68 | | } |
| | 557 | 69 | | } |
| | | 70 | | } |