< Summary

Information
Class: Elsa.Workflows.Runtime.ExecutionCycleHandle
Assembly: Elsa.Workflows.Runtime
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Runtime/Models/ExecutionCycleHandle.cs
Line coverage
97%
Covered lines: 34
Uncovered lines: 1
Coverable lines: 35
Total lines: 103
Line coverage: 97.1%
Branch coverage
90%
Covered branches: 9
Total branches: 10
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Id()100%11100%
get_WorkflowInstanceId()100%11100%
get_IngressSourceName()100%11100%
get_StartedAt()100%11100%
get_CancellationToken()100%11100%
get_Disposed()100%11100%
Cancel()100%6685.71%
Dispose()75%44100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Runtime/Models/ExecutionCycleHandle.cs

#LineLine coverage
 1using Elsa.Common;
 2
 3namespace Elsa.Workflows.Runtime;
 4
 5/// <summary>
 6/// Tracks a single in-flight execution cycle of the workflow runtime. Created when a cycle starts, disposed when
 7/// it completes or is force-cancelled during drain. Active-cycle accounting is done through
 8/// <see cref="IExecutionCycleRegistry"/>.
 9/// </summary>
 10public sealed class ExecutionCycleHandle : IDisposable
 11{
 12    private readonly CancellationTokenSource _cycleCts;
 13    private readonly Action<ExecutionCycleHandle>? _onDisposed;
 14    private readonly Action? _cancelCallback;
 57015    private readonly TaskCompletionSource _disposedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
 16    private int _cancelled;
 17    private int _disposed;
 18
 19    /// <summary>
 20    /// Creates a new handle. The owning <see cref="IExecutionCycleRegistry"/> supplies <paramref name="onDisposed"/>
 21    /// so it can decrement its active count. The optional <paramref name="cancelCallback"/> is invoked by
 22    /// <see cref="Cancel"/> to propagate cancellation into the workflow execution itself (e.g.,
 23    /// <c>WorkflowExecutionContext.Cancel()</c>) so that the running cycle stops scheduling new activities. Without it,
 24    /// <see cref="Cancel"/> only signals the cycle's own <see cref="CancellationToken"/>, which is consumed by the
 25    /// execution-cycle registry but does NOT propagate to the workflow runner's pipeline (the runner reads from
 26    /// <c>WorkflowExecutionContext.CancellationToken</c>, which is captured at context construction and is not part
 27    /// of this linked chain).
 28    /// </summary>
 57029    public ExecutionCycleHandle(
 57030        Guid id,
 57031        string workflowInstanceId,
 57032        string? ingressSourceName,
 57033        DateTimeOffset startedAt,
 57034        CancellationToken linkedToken,
 57035        Action<ExecutionCycleHandle>? onDisposed = null,
 57036        Action? cancelCallback = null)
 37    {
 57038        Id = id;
 57039        WorkflowInstanceId = workflowInstanceId;
 57040        IngressSourceName = ingressSourceName;
 57041        StartedAt = startedAt;
 57042        _cycleCts = CancellationTokenSource.CreateLinkedTokenSource(linkedToken);
 57043        _onDisposed = onDisposed;
 57044        _cancelCallback = cancelCallback;
 57045    }
 46
 47    /// <summary>Unique identifier for this execution cycle within the current runtime generation.</summary>
 112648    public Guid Id { get; }
 49
 50    /// <summary>Workflow instance whose execution this cycle is driving.</summary>
 851    public string WorkflowInstanceId { get; }
 52
 53    /// <summary>
 54    /// Name of the ingress source that initiated this cycle, when attribution is available (null for direct API invocat
 55    /// </summary>
 356    public string? IngressSourceName { get; }
 57
 58    /// <summary>When the cycle started.</summary>
 359    public DateTimeOffset StartedAt { get; }
 60
 61    /// <summary>Cancellation token passed into the workflow execution pipeline for this cycle.</summary>
 462    public CancellationToken CancellationToken => _cycleCts.Token;
 63
 64    /// <summary>
 65    /// Completes when <see cref="Dispose"/> runs — i.e., when the workflow runner finishes the cycle (cleanly or
 66    /// via cancellation) and the middleware exits its <c>using</c> block. The drain orchestrator awaits this with
 67    /// a timeout before persisting <see cref="WorkflowSubStatus.Interrupted"/>, ensuring its write happens AFTER
 68    /// any commit the runner emits in response to <see cref="Cancel"/>.
 69    /// </summary>
 670    public Task Disposed => _disposedTcs.Task;
 71
 72    /// <summary>
 73    /// Cancels the cycle — used by the drain orchestrator on deadline breach or operator force.
 74    /// Invokes the cancel callback (when supplied at construction) to propagate cancellation into the workflow
 75    /// execution, then cancels the cycle's own linked CTS. Safe to call multiple times; idempotent.
 76    /// </summary>
 77    public void Cancel()
 78    {
 979        if (_disposed != 0) return;
 80        // Idempotent guard: ensures the cancel callback and CTS cancellation run AT MOST once even if Cancel() is
 81        // called repeatedly before disposal. Without this the drain orchestrator (or any other future caller) could
 82        // accidentally trigger a non-idempotent cancellation side effect multiple times.
 883        if (Interlocked.Exchange(ref _cancelled, 1) != 0) return;
 84
 85        // Propagate to the workflow execution first (this typically marks the workflow as Cancelled and clears its
 86        // schedule, so the runner stops scheduling new activities). The orchestrator's subsequent Interrupted
 87        // persistence then overrides the Cancelled sub-status — see Disposed-await sequencing in DrainOrchestrator.
 1188        try { _cancelCallback?.Invoke(); }
 289        catch (Exception ex) when (!ex.IsFatal()) { /* Cancellation is best-effort; non-fatal failures here must not bre
 90
 1291        try { _cycleCts.Cancel(); }
 092        catch (ObjectDisposedException) { /* Race with Dispose — acceptable. */ }
 693    }
 94
 95    /// <summary>Releases the linked CTS, notifies the registry, and signals <see cref="Disposed"/>.</summary>
 96    public void Dispose()
 97    {
 55898        if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
 55699        _onDisposed?.Invoke(this);
 556100        _cycleCts.Dispose();
 556101        _disposedTcs.TrySetResult();
 556102    }
 103}