< Summary

Information
Class: Elsa.Workflows.Runtime.Middleware.Workflows.ExecutionCycleTrackingMiddleware
Assembly: Elsa.Workflows.Runtime
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/ExecutionCycleTrackingMiddleware.cs
Line coverage
78%
Covered lines: 11
Uncovered lines: 3
Coverable lines: 14
Total lines: 70
Line coverage: 78.5%
Branch coverage
50%
Covered branches: 1
Total branches: 2
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
InvokeAsync()50%2276.92%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/ExecutionCycleTrackingMiddleware.cs

#LineLine coverage
 1using Elsa.Workflows.Pipelines.WorkflowExecution;
 2
 3namespace 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>
 53119public 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    {
 55739        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.
 55747        var handle = cycleRegistry.BeginCycle(
 55748            context.Id,
 55749            ingressSourceName,
 55750            context.CancellationToken,
 55751            cancelCallback: context.Cancel);
 55752        context.TransientProperties[ExecutionCycleHandleKey] = handle;
 53
 54        try
 55        {
 55756            await Next(context);
 55757        }
 058        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.
 066            handle.Dispose();
 067            throw;
 68        }
 55769    }
 70}