| | | 1 | | namespace NexusLabs.Needlr.AgentFramework.Providers; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Declarative failure-handling rule applied by |
| | | 5 | | /// <see cref="TieredProviderSelector{TQuery, TResult}"/> when a provider throws an |
| | | 6 | | /// exception during <see cref="ITieredProvider{TQuery, TResult}.ExecuteAsync"/>. |
| | | 7 | | /// </summary> |
| | | 8 | | /// <remarks> |
| | | 9 | | /// <para> |
| | | 10 | | /// Policies are evaluated in order against the thrown exception (first match wins). The |
| | | 11 | | /// first policy whose <see cref="Match"/> predicate returns <see langword="true"/> |
| | | 12 | | /// causes the selector to: |
| | | 13 | | /// </para> |
| | | 14 | | /// <list type="number"> |
| | | 15 | | /// <item>Add a per-provider attempt diagnostic to the chain and continue to the next provider.</item> |
| | | 16 | | /// <item>Mark the provider as skipped for the duration in <see cref="SkipDuration"/> (if non-null), so subsequent cal |
| | | 17 | | /// <item>Invoke the <see cref="OnHit"/> callback (if non-null) with a <see cref="ProviderFailureContext"/> describing |
| | | 18 | | /// </list> |
| | | 19 | | /// <para> |
| | | 20 | | /// If no policy matches the thrown exception, the selector re-throws the exception |
| | | 21 | | /// unchanged. The default policy in |
| | | 22 | | /// <see cref="TieredProviderSelectorOptions.Default"/> matches |
| | | 23 | | /// <see cref="ProviderUnavailableException"/> with no skip and no callback, preserving |
| | | 24 | | /// the framework's historical fall-through behaviour. |
| | | 25 | | /// </para> |
| | | 26 | | /// <para> |
| | | 27 | | /// <b>Cancellation is not subject to policy matching:</b> the selector skips policy |
| | | 28 | | /// evaluation entirely when the active <see cref="CancellationToken"/> has been |
| | | 29 | | /// cancelled, so cancelled calls always propagate |
| | | 30 | | /// <see cref="OperationCanceledException"/> directly to the caller. |
| | | 31 | | /// </para> |
| | | 32 | | /// <para> |
| | | 33 | | /// <b>Callback exceptions propagate.</b> If <see cref="OnHit"/> throws, the selector |
| | | 34 | | /// still releases quota for the failed attempt (the release happens in a |
| | | 35 | | /// <see langword="finally"/> block) and the callback's exception escapes |
| | | 36 | | /// <see cref="ITieredProviderSelector{TQuery, TResult}.ExecuteAsync"/>. Subsequent |
| | | 37 | | /// providers are not attempted for that call. |
| | | 38 | | /// </para> |
| | | 39 | | /// </remarks> |
| | | 40 | | /// <param name="Match"> |
| | | 41 | | /// Predicate evaluated against each thrown exception to determine whether this policy |
| | | 42 | | /// applies. The first matching policy in |
| | | 43 | | /// <see cref="TieredProviderSelectorOptions.FailurePolicies"/> wins. |
| | | 44 | | /// </param> |
| | | 45 | | /// <param name="SkipDuration"> |
| | | 46 | | /// Optional duration the failing provider should be skipped before being retried on |
| | | 47 | | /// subsequent calls. |
| | | 48 | | /// <list type="bullet"> |
| | | 49 | | /// <item><see langword="null"/> — no cross-call skip; the provider is retried on the next call.</item> |
| | | 50 | | /// <item>A finite <see cref="TimeSpan"/> — the provider is skipped until <c>now + SkipDuration</c>.</item> |
| | | 51 | | /// <item><see cref="IndefiniteSkip"/> (<see cref="TimeSpan.MaxValue"/>) — the provider is skipped until process resta |
| | | 52 | | /// </list> |
| | | 53 | | /// </param> |
| | | 54 | | /// <param name="OnHit"> |
| | | 55 | | /// Optional async callback invoked after the policy match is recorded but before |
| | | 56 | | /// fall-through to the next provider. Receives a <see cref="ProviderFailureContext"/> |
| | | 57 | | /// describing the failed provider, the exception, and the resulting skip-until |
| | | 58 | | /// timestamp (if any). |
| | | 59 | | /// </param> |
| | | 60 | | /// <example> |
| | | 61 | | /// <code> |
| | | 62 | | /// // Treat ApiAuthException like ProviderUnavailableException, but skip for 5 minutes |
| | | 63 | | /// // and emit a structured log. |
| | | 64 | | /// var policy = new ProviderFailurePolicy( |
| | | 65 | | /// Match: ex => ex is ApiAuthException, |
| | | 66 | | /// SkipDuration: TimeSpan.FromMinutes(5), |
| | | 67 | | /// OnHit: ctx => |
| | | 68 | | /// { |
| | | 69 | | /// logger.LogWarning(ctx.Exception, "Provider {Provider} skipped until {Until}", |
| | | 70 | | /// ctx.ProviderName, ctx.SkipUntil); |
| | | 71 | | /// return ValueTask.CompletedTask; |
| | | 72 | | /// }); |
| | | 73 | | /// |
| | | 74 | | /// var options = TieredProviderSelectorOptions.Default with |
| | | 75 | | /// { |
| | | 76 | | /// FailurePolicies = [.. TieredProviderSelectorOptions.Default.FailurePolicies, policy], |
| | | 77 | | /// }; |
| | | 78 | | /// </code> |
| | | 79 | | /// </example> |
| | 28 | 80 | | public sealed record ProviderFailurePolicy( |
| | 49 | 81 | | Predicate<Exception> Match, |
| | 37 | 82 | | TimeSpan? SkipDuration = null, |
| | 64 | 83 | | Func<ProviderFailureContext, ValueTask>? OnHit = null) |
| | | 84 | | { |
| | | 85 | | /// <summary> |
| | | 86 | | /// Sentinel value for <see cref="SkipDuration"/> meaning "skip indefinitely |
| | | 87 | | /// (until the host process restarts)." Resolves to |
| | | 88 | | /// <see cref="DateTimeOffset.MaxValue"/> in the selector's skip cache; the |
| | | 89 | | /// selector clamps the <c>now + SkipDuration</c> addition so passing this value |
| | | 90 | | /// will not overflow. |
| | | 91 | | /// </summary> |
| | 4 | 92 | | public static TimeSpan IndefiniteSkip => TimeSpan.MaxValue; |
| | | 93 | | } |