< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Providers.ProviderFailurePolicy
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Providers/ProviderFailurePolicy.cs
Line coverage
100%
Covered lines: 5
Uncovered lines: 0
Coverable lines: 5
Total lines: 93
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
.ctor(...)100%11100%
get_Match()100%11100%
get_SkipDuration()100%11100%
get_OnHit()100%11100%
get_IndefiniteSkip()100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Providers/ProviderFailurePolicy.cs

#LineLine coverage
 1namespace 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>
 2880public sealed record ProviderFailurePolicy(
 4981    Predicate<Exception> Match,
 3782    TimeSpan? SkipDuration = null,
 6483    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>
 492    public static TimeSpan IndefiniteSkip => TimeSpan.MaxValue;
 93}