< Summary

Information
Class: Elsa.Workflows.Signals.FaultSignal
Assembly: Elsa.Workflows.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Signals/FaultSignal.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 123
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Exception()100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Signals/FaultSignal.cs

#LineLine coverage
 1namespace Elsa.Workflows.Signals;
 2
 3/// <summary>
 4/// Sent up the ancestor chain when an activity faulted, giving an enclosing container the chance to handle the failure
 5/// itself instead of letting it fall through to the workflow-global <see cref="IIncidentStrategy"/>.
 6/// </summary>
 7/// <remarks>
 8/// <para>
 9/// A container opts in exactly the way it opts into cancellation, by registering a handler for this signal:
 10/// </para>
 11/// <code>
 12/// public MyContainer()
 13/// {
 14///     OnSignalReceived&lt;FaultSignal&gt;(OnChildFaultedAsync);
 15/// }
 16///
 17/// private async ValueTask OnChildFaultedAsync(FaultSignal signal, SignalContext context)
 18/// {
 19///     if (!IsMine(signal.FaultedContext))
 20///         return;                        // Not my child: let it keep bubbling.
 21///
 22///     context.StopPropagation();         // I am handling this.
 23///     await signal.FaultedContext.CancelActivityAsync();
 24///     await ScheduleFallbackAsync(context.ReceiverActivityExecutionContext);
 25/// }
 26/// </code>
 27/// <para>
 28/// Bubbling is the default. A handler that does not recognize <see cref="FaultedContext"/> simply returns without
 29/// stopping propagation, and the signal continues to the next ancestor, so nested containers compose without any of
 30/// them knowing about the others. If no ancestor stops propagation, the incident strategy runs exactly as it does
 31/// when this signal does not exist at all.
 32/// </para>
 33/// <para>
 34/// <b>The faulting activity is itself a receiver.</b> <c>SendSignalAsync</c> delivers to the sender before walking its
 35/// ancestors, and this signal reuses that dispatch rather than introducing a variant of it. An activity that throws and
 36/// also handles <see cref="FaultSignal"/> therefore sees its own fault first, and may claim it, which is what a
 37/// self-retrying or self-compensating activity wants. This grants no ability to hide a failure that an activity did not
 38/// already have, since one that simply catches its own exception never faults at all. A handler that wants
 39/// ancestors-only semantics should check <see cref="SignalContext.IsSelf"/>, or compare <see cref="FaultedContext"/>
 40/// against its own receiver context, the same way it already checks that the faulting context is one of its children.
 41/// </para>
 42/// <para>
 43/// <b>The contract.</b> Responsibilities are split, and the split is deliberate:
 44/// </para>
 45/// <list type="bullet">
 46///   <item>
 47///     <description>
 48///     <b>The middleware</b> calls <c>Fault()</c>, sends this signal, and — only when propagation was stopped — calls
 49///     <c>RecoverFromFault()</c> exactly once. Nothing else.
 50///     </description>
 51///   </item>
 52///   <item>
 53///     <description>
 54///     <b>The handler</b> decides the fault is its own, calls <see cref="SignalContext.StopPropagation"/>, and then
 55///     terminalizes the faulted activity: cancel it, complete it, or reschedule it. There is no single right answer
 56///     for the middleware to pick here — an interrupting error boundary wants the activity cancelled, a retry handler
 57///     wants it rescheduled, a fallback handler may want it completed with a substitute result — so a handler that
 58///     claims the failure also owns deciding what becomes of the failed work.
 59///     </description>
 60///   </item>
 61///   <item>
 62///     <description>
 63///     <b>The handler must not</b> call <c>RecoverFromFault()</c>.
 64///     </description>
 65///   </item>
 66///   <item>
 67///     <description>
 68///     <b>A handler that throws is treated as not having handled the fault.</b> Its exception does not escape, because
 69///     this signal is sent from inside the <c>catch</c> whose whole job is to stop exceptions escaping the activity
 70///     pipeline; letting a handler's failure through would defeat that and lose the original fault with it. The inciden
 71///     strategy then runs exactly as it would with no handler present, which is the conservative direction: a handler
 72///     that failed part way through may have left the faulted activity in any state, and an incident is a better answer
 73///     than silence. The handler's own exception is logged at error level, because a broken fault handler is a defect i
 74///     its own right rather than a workflow outcome.
 75///     </description>
 76///   </item>
 77///   <item>
 78///     <description>
 79///     <b>Cancellation is the exception to that.</b> An <see cref="OperationCanceledException"/> from a handler
 80///     propagates, because it means the host is tearing the run down rather than that the handler is broken. Treating
 81///     it as a handler failure would turn a deliberate cancellation into a faulted workflow.
 82///     </description>
 83///   </item>
 84/// </list>
 85/// <para>
 86/// <b>Completing the faulted activity takes one extra step.</b> <c>CompleteActivityAsync</c> returns immediately unless
 87/// the activity is <see cref="ActivityStatus.Running"/>, and throughout the handler it is still
 88/// <see cref="ActivityStatus.Faulted"/>, because recovery runs only once the handler has returned. Completing it inline
 89/// therefore does nothing at all, silently. A handler that wants to complete the activity — substituting a result for
 90/// the work that failed, say — has to move it out of the faulted state first:
 91/// </para>
 92/// <code>
 93/// context.StopPropagation();
 94/// signal.FaultedContext.TransitionTo(ActivityStatus.Running);
 95/// await signal.FaultedContext.CompleteActivityAsync(substituteResult);
 96/// </code>
 97/// <para>
 98/// This is not licence to call <c>RecoverFromFault()</c>, which also rewrites the fault counts and remains the
 99/// middleware's job alone. Completing this way fires the enclosing container's completion callback, so the container's
 100/// normal sequencing resumes. Cancelling needs no equivalent step, because <c>CancelActivityAsync</c> already accepts a
 101/// faulted activity, and rescheduling needs none either.
 102/// </para>
 103/// <para>
 104/// That last rule is not stylistic. <c>RecoverFromFault()</c> is asymmetric: it <i>sets</i> the faulting context's
 105/// <see cref="ActivityExecutionContext.AggregateFaultCount"/> to zero, which is idempotent, but <i>decrements</i> the
 106/// count on every ancestor, which is not. A second call is therefore harmless for the faulting context and harmful for
 107/// every ancestor, driving their counts to <c>-1</c>. Those counts are persisted and surface through
 108/// <c>ActivityExecutionRecord.AggregateFaultCount</c> and <c>ActivityExecutionStats</c>, so the failure mode is
 109/// silently wrong fault numbers in the UI rather than an exception anyone would notice.
 110/// </para>
 111/// <para>
 112/// <b>On the faulted activity's status.</b> <c>RecoverFromFault()</c> transitions the activity out of
 113/// <see cref="ActivityStatus.Faulted"/> and back to <see cref="ActivityStatus.Running"/>, so a handler that stops
 114/// propagation and terminalizes nothing leaves the activity <see cref="ActivityStatus.Running"/> having already thrown.
 115/// That is the handler's bug. A completing container happens to sweep such an activity via
 116/// <see cref="ActivityExecutionContext.CompleteActivityAsync"/>, which cancels its non-completed children, so the
 117/// mistake degrades rather than hangs — but that is a backstop, not the mechanism, and a container that suspends
 118/// instead of completing will persist the activity as <see cref="ActivityStatus.Running"/>.
 119/// </para>
 120/// </remarks>
 121/// <param name="Exception">The exception that caused the fault.</param>
 122/// <param name="FaultedContext">The <see cref="ActivityExecutionContext"/> of the activity that faulted.</param>
 14123public record FaultSignal(Exception Exception, ActivityExecutionContext FaultedContext);

Methods/Properties

get_Exception()