| | | 1 | | namespace AsyncResponse; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Raised by a publish whose response (or exception) found no live subscriber and whose |
| | | 5 | | /// lost-subscriber recovery callbacks could not all be invoked <em>transiently</em>. The message |
| | | 6 | | /// was <b>not</b> acknowledged — every registration whose callback did not succeed stays armed, |
| | | 7 | | /// and the broker ingress propagates this exception untouched (no <c>SetException</c> escalation, |
| | | 8 | | /// which would only re-invoke the same failing callback, or fail flows whose resume merely |
| | | 9 | | /// blipped) so the transport redelivers the message under its own bounded policy |
| | | 10 | | /// (<c>MaxDeliveryAttempts</c>, then the dead-letter destination). The terminal signal therefore |
| | | 11 | | /// survives in the broker until the callback's dependency recovers or an operator replays it from |
| | | 12 | | /// the dead-letter queue, instead of being acknowledged into a log line while the flow stays stuck. |
| | | 13 | | /// <para>Two paths raise it:</para> |
| | | 14 | | /// <list type="bullet"> |
| | | 15 | | /// <item><description>the <b>failure</b> callback failed on every attempt of its in-process retry |
| | | 16 | | /// ladder (<see cref="Attempts"/> is that ladder's length);</description></item> |
| | | 17 | | /// <item><description>any resume or exception callback failed transiently, including a single |
| | | 18 | | /// registration or a fan-out in which none succeeded (<see cref="Attempts"/> is 1 — transport |
| | | 19 | | /// redelivery owns the retry). Successful siblings are deleted; failed registrations stay armed.</description></item> |
| | | 20 | | /// </list> |
| | | 21 | | /// <para> |
| | | 22 | | /// Deterministic callback faults — an unauthorized or unresolvable target, a method that no longer |
| | | 23 | | /// binds — are never wrapped in this type. In a partially successful fan-out they are logged |
| | | 24 | | /// and retained for watchdog visibility; when every callback fails deterministically, the |
| | | 25 | | /// original failure propagates to the ingress's exception-routing policy. |
| | | 26 | | /// </para> |
| | | 27 | | /// <para> |
| | | 28 | | /// A direct caller of <c>IAsyncResponsePublisher.SetResponse</c>/<c>SetException</c> (an HTTP |
| | | 29 | | /// callback endpoint, for instance) sees this exception too; answering the remote system with a |
| | | 30 | | /// retriable status is the equivalent of the broker's redelivery. |
| | | 31 | | /// </para> |
| | | 32 | | /// </summary> |
| | | 33 | | public sealed class RecoveryCallbackFailedException : Exception |
| | | 34 | | { |
| | | 35 | | /// <summary>Creates the exception for <paramref name="correlationId"/> after <paramref name="attempts"/> failed inv |
| | | 36 | | public RecoveryCallbackFailedException(string correlationId, int attempts, Exception innerException) |
| | 50 | 37 | | : base( |
| | 50 | 38 | | $"A lost-subscriber recovery callback for correlationId '{correlationId}' failed transiently ({attempts} att |
| | 50 | 39 | | "the message was not acknowledged so the transport can redeliver it to the registration(s) still armed.", |
| | 50 | 40 | | innerException) |
| | | 41 | | { |
| | 50 | 42 | | CorrelationId = correlationId; |
| | 50 | 43 | | Attempts = attempts; |
| | 50 | 44 | | } |
| | | 45 | | |
| | | 46 | | /// <summary>The correlation id whose recovery callback could not be invoked.</summary> |
| | 34 | 47 | | public string CorrelationId { get; } |
| | | 48 | | |
| | | 49 | | /// <summary>How many in-process invocations were attempted before handing the delivery back to the transport (1 whe |
| | 4 | 50 | | public int Attempts { get; } |
| | | 51 | | } |