| | | 1 | | namespace 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<FaultSignal>(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> |
| | 14 | 123 | | public record FaultSignal(Exception Exception, ActivityExecutionContext FaultedContext); |