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

Information
Class: AsyncResponse.Channels.NATS.NatsAsyncResponseChannelOptions
Assembly: AsyncResponse.Channels.NATS
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Channels/AsyncResponse.Channels.NATS/NatsAsyncResponseChannelOptions.cs
Line coverage
100%
Covered lines: 29
Uncovered lines: 0
Coverable lines: 29
Total lines: 115
Line coverage: 100%
Branch coverage
100%
Covered branches: 20
Total branches: 20
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%
Validate()100%1616100%
Required(...)100%22100%
Positive(...)100%22100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Channels/AsyncResponse.Channels.NATS/NatsAsyncResponseChannelOptions.cs

#LineLine coverage
 1namespace AsyncResponse.Channels.NATS;
 2
 3/// <summary>
 4/// Options for the NATS-backed async-response channel.
 5/// <para>
 6/// The channel delivers responses over NATS Core <em>request/reply</em>: a waiter subscribes to a
 7/// per-correlation subject and replies to confirm receipt, and the publisher uses a request so the
 8/// NATS "no responders" signal tells it precisely when nobody is listening (the moment that triggers
 9/// lost-subscriber recovery). Durable <see cref="RecoveryState"/> lives in a NATS JetStream
 10/// Key-Value bucket so a response that arrives after the waiter died (e.g. a redeploy) can still be
 11/// routed to the resume or failure callback.
 12/// </para>
 13/// </summary>
 14public sealed class NatsAsyncResponseChannelOptions : DurableAsyncResponseChannelOptions
 15{
 16    /// <summary>The channel name reported to the startup validator.</summary>
 17    public const string ChannelName = "NATS";
 18
 19    /// <summary>
 20    /// Subject prefix for every response subject created by the channel. A response subject is
 21    /// <c>{SubjectPrefix}.response.{encodedCorrelationId}</c>, where the correlation id is encoded
 22    /// to a NATS-safe token. Change it to isolate multiple applications or environments sharing one
 23    /// NATS system. Treat it as a deployment-wide contract: publishers and subscribers must agree on
 24    /// it.
 25    /// </summary>
 326    public string SubjectPrefix { get; set; } = "asyncresponse";
 27
 28    /// <summary>
 29    /// Name of the JetStream Key-Value bucket that stores durable <see cref="RecoveryState"/>.
 30    /// Must be a valid bucket name (alphanumeric, dash, underscore). Changing it orphans existing
 31    /// recovery state. The backing JetStream stream is <c>KV_{RecoveryBucket}</c>.
 32    /// </summary>
 333    public string RecoveryBucket { get; set; } = "asyncresponse-recovery";
 34
 35    /// <summary>
 36    /// Replica count for the recovery Key-Value bucket. Use a value greater than <c>1</c> on a NATS
 37    /// cluster so recovery state survives a single node loss. Default: <c>1</c>.
 38    /// </summary>
 339    public int RecoveryBucketReplicas { get; set; } = 1;
 40
 41    // RecoveryStateExpiry and DefaultTimeout are inherited from AsyncResponseChannelOptions (the NATS
 42    // expiry is also applied as the Key-Value bucket's MaxAge ceiling — see the recovery store).
 43
 44    /// <summary>
 45    /// How long a publish waits for a waiter to acknowledge receipt before concluding that, although
 46    /// at least one subscriber had interest, none confirmed in time — which is still treated as
 47    /// <em>delivered</em> (the live subscriber received the message; only its ack was slow). The
 48    /// definitive "nobody is listening" signal is NATS no-responders, which returns immediately and
 49    /// is independent of this timeout. Keep it short. Default: 5 seconds.
 50    /// </summary>
 351    public TimeSpan DeliveryConfirmationTimeout { get; set; } = TimeSpan.FromSeconds(5);
 52
 53    /// <summary>
 54    /// How long the active-subscriber probe (used by the watchdog) waits for a live waiter to answer
 55    /// a presence ping before reporting zero live subscribers. NATS Core does not expose exact
 56    /// subscriber counts to clients, so the probe reports presence (0 or 1). Default: 2 seconds.
 57    /// </summary>
 358    public TimeSpan PresenceProbeTimeout { get; set; } = TimeSpan.FromSeconds(2);
 59
 60    // IncludeRemoteStackTrace and MaxRemoteStackTraceLength are inherited from
 61    // DurableAsyncResponseChannelOptions.
 62
 63    /// <summary>
 64    /// Validates the options, throwing <see cref="InvalidOperationException"/> on a misconfiguration.
 65    /// Called by the channel, the recovery store, and the DI registration so a bad configuration
 66    /// fails fast rather than at first use.
 67    /// </summary>
 68    public void Validate()
 69    {
 70        // Shared channel knobs (RecoveryStateExpiry, DefaultTimeout, DisposalDrainTimeout) go
 71        // through the ONE base guard set — a bespoke duplicate here silently missed every knob
 72        // added to the base later (DisposalDrainTimeout was validated nowhere on this provider).
 373        ValidateShared(nameof(NatsAsyncResponseChannelOptions));
 74
 375        Required(SubjectPrefix, nameof(SubjectPrefix));
 376        Required(RecoveryBucket, nameof(RecoveryBucket));
 77
 378        if (RecoveryBucketReplicas <= 0)
 379            throw new InvalidOperationException($"{nameof(NatsAsyncResponseChannelOptions)}.{nameof(RecoveryBucketReplic
 80
 381        Positive(DeliveryConfirmationTimeout, nameof(DeliveryConfirmationTimeout));
 382        Positive(PresenceProbeTimeout, nameof(PresenceProbeTimeout));
 83
 384        if (MaxRemoteStackTraceLength < 0)
 385            throw new InvalidOperationException($"{nameof(NatsAsyncResponseChannelOptions)}.{nameof(MaxRemoteStackTraceL
 86
 87        // A NATS bucket name must be a single token of [A-Za-z0-9_-]. A dotted/whitespace value would
 88        // silently produce an unusable backing stream, so reject it explicitly.
 389        foreach (var c in RecoveryBucket)
 90        {
 391            if (!(char.IsAsciiLetterOrDigit(c) || c is '-' or '_'))
 392                throw new InvalidOperationException(
 393                    $"{nameof(NatsAsyncResponseChannelOptions)}.{nameof(RecoveryBucket)} '{RecoveryBucket}' is not a val
 394                    "(allowed characters: letters, digits, '-', '_').");
 95        }
 96
 97        // A subject prefix becomes leading tokens of every response subject; it must not contain the
 98        // NATS subject wildcards or separators that would break addressing.
 399        if (SubjectPrefix.IndexOfAny([' ', '\t', '*', '>', '\r', '\n']) >= 0)
 3100            throw new InvalidOperationException(
 3101                $"{nameof(NatsAsyncResponseChannelOptions)}.{nameof(SubjectPrefix)} '{SubjectPrefix}' must not contain w
 3102    }
 103
 104    private static void Required(string? value, string name)
 105    {
 3106        if (string.IsNullOrWhiteSpace(value))
 3107            throw new InvalidOperationException($"{nameof(NatsAsyncResponseChannelOptions)}.{name} must be configured.")
 3108    }
 109
 110    private static void Positive(TimeSpan value, string name)
 111    {
 3112        if (value <= TimeSpan.Zero)
 3113            throw new InvalidOperationException($"{nameof(NatsAsyncResponseChannelOptions)}.{name} must be positive.");
 3114    }
 115}