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

Information
Class: AsyncResponse.Channels.MongoDB.MongoDbAsyncResponseChannelOptions
Assembly: AsyncResponse.Channels.MongoDB
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Channels/AsyncResponse.Channels.MongoDB/MongoDbAsyncResponseChannelOptions.cs
Line coverage
100%
Covered lines: 50
Uncovered lines: 0
Coverable lines: 50
Total lines: 162
Line coverage: 100%
Branch coverage
100%
Covered branches: 18
Total branches: 18
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%
Positive(...)100%22100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Channels/AsyncResponse.Channels.MongoDB/MongoDbAsyncResponseChannelOptions.cs

#LineLine coverage
 1namespace AsyncResponse.Channels.MongoDB;
 2
 3/// <summary>
 4/// Options for the MongoDB-backed async-response channel.
 5/// <para>
 6/// Active waiters are woken with a MongoDB change stream watching inserts to the response-message
 7/// collection; the change event carries the correlation id, so only the signaled waiter's messages
 8/// are scanned. Response envelopes are stored as documents, and durable <see cref="RecoveryState"/>
 9/// entries live in a TTL-indexed collection so late responses can resume or fail flows after the
 10/// original waiter process dies. Change streams require the server to run as a replica set (a
 11/// single-node replica set is sufficient); without one the channel degrades to interval polling.
 12/// </para>
 13/// </summary>
 14public sealed class MongoDbAsyncResponseChannelOptions : DurableAsyncResponseChannelOptions
 15{
 16    /// <summary>The channel name reported to the startup validator.</summary>
 17    public const string ChannelName = "MongoDB";
 18
 19    /// <summary>
 20    /// Optional MongoDB connection string used when no <c>IMongoDatabase</c> or <c>IMongoClient</c>
 21    /// is registered with the host.
 22    /// </summary>
 23    public string? ConnectionString { get; set; }
 24
 25    /// <summary>Optional database name used when no <c>IMongoDatabase</c> is registered.</summary>
 26    public string? DatabaseName { get; set; }
 27
 28    /// <summary>
 29    /// Collection storing durable recovery registrations. Each waiter registration is one document
 30    /// keyed by correlation id and registration id, expired natively by a TTL index.
 31    /// </summary>
 332    public string RecoveryStateCollection { get; set; } = "asyncresponse_recovery_state";
 33
 34    /// <summary>
 35    /// Collection storing response envelopes until they expire. The change stream watches inserts to
 36    /// this collection; waiters load the envelope documents from it.
 37    /// </summary>
 338    public string MessageCollection { get; set; } = "asyncresponse_channel_messages";
 39
 40    /// <summary>
 41    /// Collection storing short-lived live-subscriber heartbeats for watchdog liveness and the
 42    /// publish fast path, expired natively by a TTL index.
 43    /// </summary>
 344    public string SubscriberCollection { get; set; } = "asyncresponse_channel_subscribers";
 45
 46    /// <summary>
 47    /// Creates the TTL and lookup indexes on first use. Disable when provisioning owns index DDL.
 48    /// </summary>
 349    public bool AutoCreateIndexes { get; set; } = true;
 50
 51    /// <summary>
 52    /// Watches the message collection with a change stream so active waiters are woken with
 53    /// broker-grade latency. Requires a replica set. When disabled — or when the server reports
 54    /// change streams as unsupported — waiters fall back to <see cref="ListenerPollInterval"/>
 55    /// polling. Default: <c>true</c>.
 56    /// </summary>
 357    public bool UseChangeStreams { get; set; } = true;
 58
 59    /// <summary>
 60    /// How long response-envelope documents are retained for active waiter delivery and
 61    /// missed-notification recovery. Expired documents are reaped by the TTL index.
 62    /// </summary>
 363    public TimeSpan MessageRetention { get; set; } = TimeSpan.FromHours(1);
 64
 65    /// <summary>
 66    /// How long a publisher waits for a live waiter to acknowledge loading a response envelope
 67    /// before treating the response as lost-subscriber delivery. Default: 5 seconds.
 68    /// </summary>
 369    public TimeSpan DeliveryConfirmationTimeout { get; set; } = TimeSpan.FromSeconds(5);
 70
 71    /// <summary>
 72    /// Poll interval used while a publisher waits for delivery acknowledgement. Default: 50 ms.
 73    /// </summary>
 374    public TimeSpan DeliveryConfirmationPollInterval { get; set; } = TimeSpan.FromMilliseconds(50);
 75
 76    /// <summary>
 77    /// Fallback poll interval used by the dispatch loop to catch messages if a change-stream event
 78    /// is missed during reconnect (or change streams are unavailable). Default: 250 ms.
 79    /// </summary>
 380    public TimeSpan ListenerPollInterval { get; set; } = TimeSpan.FromMilliseconds(250);
 81
 82    /// <summary>
 83    /// Number of pending response messages loaded per subscribed correlation id per dispatch pass.
 84    /// Default: 64.
 85    /// </summary>
 386    public int PendingMessageBatchSize { get; set; } = 64;
 87
 88    /// <summary>
 89    /// How often a live waiter refreshes its subscriber heartbeat document. Default: 10 seconds.
 90    /// </summary>
 391    public TimeSpan SubscriberHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
 92
 93    /// <summary>
 94    /// How long a subscriber heartbeat remains live without refresh. Keep this above
 95    /// <see cref="SubscriberHeartbeatInterval"/>. Default: 30 seconds.
 96    /// </summary>
 397    public TimeSpan SubscriberHeartbeatTimeout { get; set; } = TimeSpan.FromSeconds(30);
 98
 99    /// <summary>Maximum attempts for a response-document insert. Set to 1 to disable publish retries. Default: 3.</summ
 3100    public int PublishMaxAttempts { get; set; } = 3;
 101
 102    /// <summary>Initial delay before retrying a failed response-document insert. Default: 50 ms.</summary>
 3103    public TimeSpan PublishRetryBaseDelay { get; set; } = TimeSpan.FromMilliseconds(50);
 104
 105    /// <summary>Maximum delay between response-document insert retries. Default: 1 second.</summary>
 3106    public TimeSpan PublishRetryMaxDelay { get; set; } = TimeSpan.FromSeconds(1);
 107
 108    /// <summary>Validates the option values and throws on misconfiguration.</summary>
 109    public void Validate()
 110    {
 111        // Shared channel knobs (RecoveryStateExpiry, DefaultTimeout, DisposalDrainTimeout) go
 112        // through the ONE base guard set — a bespoke duplicate here silently missed every knob
 113        // added to the base later (DisposalDrainTimeout was validated nowhere on this provider).
 3114        ValidateShared(nameof(MongoDbAsyncResponseChannelOptions));
 115
 3116        MongoDbChannelStore.ValidateCollectionName(RecoveryStateCollection, nameof(RecoveryStateCollection));
 3117        MongoDbChannelStore.ValidateCollectionName(MessageCollection, nameof(MessageCollection));
 3118        MongoDbChannelStore.ValidateCollectionName(SubscriberCollection, nameof(SubscriberCollection));
 119
 3120        if (StringComparer.Ordinal.Equals(RecoveryStateCollection, MessageCollection)
 3121            || StringComparer.Ordinal.Equals(RecoveryStateCollection, SubscriberCollection)
 3122            || StringComparer.Ordinal.Equals(MessageCollection, SubscriberCollection))
 123        {
 3124            throw new InvalidOperationException(
 3125                $"{nameof(MongoDbAsyncResponseChannelOptions)}.{nameof(RecoveryStateCollection)}, " +
 3126                $"{nameof(MessageCollection)}, and {nameof(SubscriberCollection)} must be distinct collections.");
 127        }
 128
 3129        Positive(MessageRetention, nameof(MessageRetention));
 3130        Positive(DeliveryConfirmationTimeout, nameof(DeliveryConfirmationTimeout));
 3131        Positive(DeliveryConfirmationPollInterval, nameof(DeliveryConfirmationPollInterval));
 3132        Positive(ListenerPollInterval, nameof(ListenerPollInterval));
 3133        Positive(SubscriberHeartbeatInterval, nameof(SubscriberHeartbeatInterval));
 3134        Positive(SubscriberHeartbeatTimeout, nameof(SubscriberHeartbeatTimeout));
 135
 3136        if (MaxRemoteStackTraceLength < 0)
 3137            throw new InvalidOperationException($"{nameof(MongoDbAsyncResponseChannelOptions)}.{nameof(MaxRemoteStackTra
 138
 3139        if (PendingMessageBatchSize <= 0)
 3140            throw new InvalidOperationException($"{nameof(MongoDbAsyncResponseChannelOptions)}.{nameof(PendingMessageBat
 141
 3142        if (SubscriberHeartbeatInterval >= SubscriberHeartbeatTimeout)
 3143            throw new InvalidOperationException(
 3144                $"{nameof(MongoDbAsyncResponseChannelOptions)}.{nameof(SubscriberHeartbeatInterval)} must be less than "
 3145                $"{nameof(MongoDbAsyncResponseChannelOptions)}.{nameof(SubscriberHeartbeatTimeout)}.");
 146
 3147        if (PublishMaxAttempts <= 0)
 3148            throw new InvalidOperationException($"{nameof(MongoDbAsyncResponseChannelOptions)}.{nameof(PublishMaxAttempt
 149
 3150        Positive(PublishRetryBaseDelay, nameof(PublishRetryBaseDelay));
 3151        Positive(PublishRetryMaxDelay, nameof(PublishRetryMaxDelay));
 3152        if (PublishRetryBaseDelay > PublishRetryMaxDelay)
 3153            throw new InvalidOperationException(
 3154                $"{nameof(MongoDbAsyncResponseChannelOptions)}.{nameof(PublishRetryBaseDelay)} cannot exceed {nameof(Pub
 3155    }
 156
 157    private static void Positive(TimeSpan value, string name)
 158    {
 3159        if (value <= TimeSpan.Zero)
 3160            throw new InvalidOperationException($"{nameof(MongoDbAsyncResponseChannelOptions)}.{name} must be positive."
 3161    }
 162}