< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.AsyncResponseChannelOptions
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseChannelOptions.cs
Line coverage
100%
Covered lines: 27
Uncovered lines: 0
Coverable lines: 27
Total lines: 127
Line coverage: 100%
Branch coverage
100%
Covered branches: 22
Total branches: 22
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
.cctor()100%11100%
EnsureWaiterTimeoutSupported(...)100%44100%
CeilingRule()100%11100%
ValidateShared(...)100%1818100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseChannelOptions.cs

#LineLine coverage
 1namespace AsyncResponse;
 2
 3/// <summary>
 4/// Shared options for an async-response channel (the response/recovery substrate). Concrete channels
 5/// extend this with their own transport-specific settings (key/subject prefixes, bucket names, …).
 6/// </summary>
 7public abstract class AsyncResponseChannelOptions
 8{
 9    /// <summary>
 10    /// How long persisted <see cref="RecoveryState"/> entries live, bounding how long after a
 11    /// crash/redeploy a late response can still trigger the lost-subscriber callbacks. Set it
 12    /// comfortably above your longest-running flow. (For the in-memory channel this is process-local
 13    /// and lost on exit.)
 14    /// </summary>
 315    public TimeSpan RecoveryStateExpiry { get; set; } = TimeSpan.FromDays(7);
 16
 17    /// <summary>
 18    /// Default timeout applied to waiters that do not specify <c>WithTimeout</c>. When <c>null</c>
 19    /// (the default), <see cref="RecoveryStateExpiry"/> is used — waits are never infinite, so a
 20    /// response that never arrives faults the waiter with a <see cref="TimeoutException"/> instead of
 21    /// hanging forever.
 22    /// </summary>
 23    public TimeSpan? DefaultTimeout { get; set; }
 24
 25    /// <summary>
 26    /// How long disposing a waiter may DRAIN an in-flight delivery before abandoning it. Disposal
 27    /// settles the response task only after a dispatch that already claimed a message — an
 28    /// <c>Until</c> predicate running user code — has finished, so a delivered response is never
 29    /// reported as canceled. If that dispatch is still running when this budget lapses, the
 30    /// response task is faulted with <see cref="AsyncResponseIndeterminateDeliveryException"/>
 31    /// rather than canceled: the delivery outcome is unknown, and a cancellation would invite
 32    /// re-attaching to a correlation id whose response may already be consumed (durable flows
 33    /// instead restart the idempotent step fresh). Default: 30 seconds — comfortably above any
 34    /// healthy predicate, well below a stuck one holding host shutdown hostage.
 35    /// </summary>
 336    public TimeSpan DisposalDrainTimeout { get; set; } = TimeSpan.FromSeconds(30);
 37
 38    /// <summary>
 39    /// The largest delay the BCL's timer plumbing accepts (<c>uint.MaxValue - 1</c> ms, ~49.7
 40    /// days): CancellationTokenSource timers, <see cref="Task.Delay(TimeSpan)"/>, and
 41    /// <c>Task.WaitAsync</c> all reject longer values at arming — which, for a waiter, is AFTER
 42    /// the subscription and recovery state already exist. Timer-armed values are therefore
 43    /// bounded here and at waiter creation instead.
 44    /// </summary>
 345    internal static readonly TimeSpan MaxTimerBackedTimeout = TimeSpan.FromMilliseconds(uint.MaxValue - 1);
 46
 47    /// <summary>
 48    /// Upper bound for persisted-TTL knobs (10 years): far beyond any practical retention, small
 49    /// enough that the "now + TTL" expiry stamp every store computes can never overflow
 50    /// <see cref="DateTime"/>/<see cref="DateTimeOffset"/> arithmetic — a
 51    /// <see cref="TimeSpan.MaxValue"/> expiry used to pass validation and then throw
 52    /// <see cref="ArgumentOutOfRangeException"/> at the first recovery-state save.
 53    /// </summary>
 354    internal static readonly TimeSpan MaxPersistenceTtl = TimeSpan.FromDays(3650);
 55
 56    /// <summary>
 57    /// Guards a RESOLVED per-waiter timeout (explicit, <see cref="DefaultTimeout"/>, or the
 58    /// <see cref="RecoveryStateExpiry"/> fallback) before any subscribe/persist side effect:
 59    /// positive (one rule for every channel — zero and the never-firing -1 ms sentinel included)
 60    /// and under the timer ceiling.
 61    /// </summary>
 62    internal static void EnsureWaiterTimeoutSupported(TimeSpan timeout)
 63    {
 364        if (timeout <= TimeSpan.Zero || timeout > MaxTimerBackedTimeout)
 365            throw new ArgumentOutOfRangeException(
 366                nameof(timeout),
 367                timeout,
 368                $"Waiter timeout must be positive and at most {MaxTimerBackedTimeout.TotalDays:0.#} days (the .NET timer
 369    }
 70
 71    /// <summary>
 72    /// Validates the shared channel settings, throwing an actionable
 73    /// <see cref="InvalidOperationException"/> on misconfiguration. Concrete channels call this from
 74    /// their own <c>Validate()</c> so every channel fails fast at registration/startup instead of
 75    /// misbehaving at the first wait (a non-positive expiry silently disables recovery; a bad
 76    /// timer-armed timeout otherwise surfaces only after waiter-registration side effects).
 77    /// </summary>
 78    internal void ValidateShared(string optionsName)
 79    {
 80        static string CeilingRule(string optionsName, string knob)
 281            => $"{optionsName}.{knob} must be positive and at most {MaxTimerBackedTimeout.TotalDays:0.#} days (the .NET 
 82
 383        if (RecoveryStateExpiry <= TimeSpan.Zero)
 384            throw new InvalidOperationException($"{optionsName}.{nameof(RecoveryStateExpiry)} must be positive.");
 385        if (RecoveryStateExpiry > MaxPersistenceTtl)
 386            throw new InvalidOperationException(
 387                $"{optionsName}.{nameof(RecoveryStateExpiry)} must be at most {MaxPersistenceTtl.TotalDays:0} days — " +
 388                "expiry stamps are computed as \"now + expiry\", and larger values overflow at the first save.");
 89
 90        // The ceiling applies to the expiry only in its TIMER-ARMED role — the waiter-timeout
 91        // fallback when DefaultTimeout is not configured. As a pure persistence TTL (with a
 92        // DefaultTimeout set) it may legitimately exceed it: e.g. 90-day recovery retention with
 93        // a 12-hour default timeout.
 394        if (DefaultTimeout is null && RecoveryStateExpiry > MaxTimerBackedTimeout)
 395            throw new InvalidOperationException(
 396                $"{optionsName}.{nameof(RecoveryStateExpiry)} is the waiter-timeout fallback while {nameof(DefaultTimeou
 397                $"not configured, and must then be at most {MaxTimerBackedTimeout.TotalDays:0.#} days (the .NET timer ce
 398                $"Configure {nameof(DefaultTimeout)} to keep a longer recovery retention.");
 99
 3100        if (DefaultTimeout is { } defaultTimeout && (defaultTimeout <= TimeSpan.Zero || defaultTimeout > MaxTimerBackedT
 3101            throw new InvalidOperationException(CeilingRule(optionsName, nameof(DefaultTimeout)));
 3102        if (DisposalDrainTimeout <= TimeSpan.Zero || DisposalDrainTimeout > MaxTimerBackedTimeout)
 3103            throw new InvalidOperationException(CeilingRule(optionsName, nameof(DisposalDrainTimeout)));
 3104    }
 105}
 106
 107/// <summary>
 108/// Shared options for a <em>durable</em> async-response channel that serializes failures onto the
 109/// wire (Redis, NATS). Adds the remote stack-trace policy on top of
 110/// <see cref="AsyncResponseChannelOptions"/>.
 111/// </summary>
 112public abstract class DurableAsyncResponseChannelOptions : AsyncResponseChannelOptions
 113{
 114    /// <summary>
 115    /// Whether a failed response envelope carries the remote exception's stack trace on the wire
 116    /// (surfaced to the waiter via <c>Exception.Data["RemoteStackTrace"]</c>). Stack traces aid
 117    /// debugging but can carry file paths; set to <c>false</c> to omit them. Default: <c>true</c>.
 118    /// </summary>
 119    public bool IncludeRemoteStackTrace { get; set; } = true;
 120
 121    /// <summary>
 122    /// Maximum length, in characters, of a remote stack trace placed on the wire and restored on the
 123    /// waiter side; longer traces are truncated with a marker. Bounds what a buggy or hostile remote
 124    /// can push into logs (a multi-megabyte trace). Applied on both publish and receive. Default: 16384.
 125    /// </summary>
 126    public int MaxRemoteStackTraceLength { get; set; } = 16 * 1024;
 127}