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

Information
Class: AsyncResponse.Channels.SqlServer.SqlServerAsyncResponseChannelOptions
Assembly: AsyncResponse.Channels.SqlServer
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Channels/AsyncResponse.Channels.SqlServer/SqlServerAsyncResponseChannelOptions.cs
Line coverage
100%
Covered lines: 56
Uncovered lines: 0
Coverable lines: 56
Total lines: 174
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.SqlServer/SqlServerAsyncResponseChannelOptions.cs

#LineLine coverage
 1namespace AsyncResponse.Channels.SqlServer;
 2
 3/// <summary>
 4/// Options for the Microsoft SQL Server-backed async-response channel.
 5/// <para>
 6/// SQL Server has no <c>LISTEN/NOTIFY</c>, so active waiters are woken by an adaptive polling sweep:
 7/// while any waiter is subscribed the dispatch loop scans the message table every
 8/// <see cref="ActivePollInterval"/>, and with no waiters it backs off to <see cref="IdlePollInterval"/>.
 9/// Same-process publishes bypass the sweep and deliver immediately. Response envelopes are stored in
 10/// a table; durable <see cref="RecoveryState"/> entries live in a separate table so late responses
 11/// can resume or fail flows after the original waiter process dies.
 12/// </para>
 13/// </summary>
 14public sealed class SqlServerAsyncResponseChannelOptions : DurableAsyncResponseChannelOptions
 15{
 16    /// <summary>The channel name reported to the startup validator.</summary>
 17    public const string ChannelName = "SqlServer";
 18
 19    /// <summary>
 20    /// SQL Server connection string used for every channel operation. Required. The database it
 21    /// targets must already exist; the channel creates only its schema, tables, and indexes.
 22    /// </summary>
 23    public string? ConnectionString { get; set; }
 24
 25    /// <summary>Database schema that contains the channel tables. Default: <c>dbo</c>.</summary>
 326    public string SchemaName { get; set; } = "dbo";
 27
 28    /// <summary>
 29    /// Table storing durable recovery registrations. Each waiter registration is one row keyed by
 30    /// correlation id and registration id.
 31    /// </summary>
 332    public string RecoveryStateTable { get; set; } = "asyncresponse_recovery_state";
 33
 34    /// <summary>
 35    /// Table storing response envelopes until they expire. The adaptive polling sweep loads pending
 36    /// envelopes from this table and delivers them to local waiters.
 37    /// </summary>
 338    public string MessageTable { get; set; } = "asyncresponse_channel_messages";
 39
 40    /// <summary>
 41    /// Table storing short-lived live-subscriber heartbeats for watchdog liveness and the publish
 42    /// fast path.
 43    /// </summary>
 344    public string SubscriberTable { get; set; } = "asyncresponse_channel_subscribers";
 45
 46    /// <summary>
 47    /// Creates the schema, tables, and indexes on first use. Disable when migrations provision them
 48    /// out of band.
 49    /// </summary>
 350    public bool AutoCreateSchema { get; set; } = true;
 51
 52    /// <summary>
 53    /// How long response-envelope rows are retained for active waiter delivery and cross-process
 54    /// sweep recovery. Expired rows are pruned opportunistically during channel operations.
 55    /// </summary>
 356    public TimeSpan MessageRetention { get; set; } = TimeSpan.FromHours(1);
 57
 58    /// <summary>
 59    /// How long a publisher waits for a live waiter to acknowledge loading a response envelope
 60    /// before treating the response as lost-subscriber delivery. Default: 5 seconds.
 61    /// </summary>
 362    public TimeSpan DeliveryConfirmationTimeout { get; set; } = TimeSpan.FromSeconds(5);
 63
 64    /// <summary>
 65    /// Poll interval used while a publisher waits for delivery acknowledgement. Default: 50 ms.
 66    /// </summary>
 367    public TimeSpan DeliveryConfirmationPollInterval { get; set; } = TimeSpan.FromMilliseconds(50);
 68
 69    /// <summary>
 70    /// Sweep interval used by the dispatch loop while at least one waiter is subscribed. This bounds
 71    /// the wake latency of a response published by another process, so keep it tight. Same-process
 72    /// deliveries do not wait for the sweep. Default: 250 ms.
 73    /// </summary>
 374    public TimeSpan ActivePollInterval { get; set; } = TimeSpan.FromMilliseconds(250);
 75
 76    /// <summary>
 77    /// Sweep interval used by the dispatch loop while no waiters are subscribed, so an idle
 78    /// application does not hammer the database. Must be at least <see cref="ActivePollInterval"/>;
 79    /// a new waiter re-arms the tight interval immediately. Default: 2 seconds.
 80    /// </summary>
 381    public TimeSpan IdlePollInterval { get; set; } = TimeSpan.FromSeconds(2);
 82
 83    /// <summary>
 84    /// Number of pending response messages loaded per subscribed correlation id per sweep pass.
 85    /// Default: 64.
 86    /// </summary>
 387    public int PendingMessageBatchSize { get; set; } = 64;
 88
 89    /// <summary>
 90    /// How often a live waiter refreshes its subscriber heartbeat row. Default: 10 seconds.
 91    /// </summary>
 392    public TimeSpan SubscriberHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
 93
 94    /// <summary>
 95    /// How long a subscriber heartbeat remains live without refresh. Keep this above
 96    /// <see cref="SubscriberHeartbeatInterval"/>. Default: 30 seconds.
 97    /// </summary>
 398    public TimeSpan SubscriberHeartbeatTimeout { get; set; } = TimeSpan.FromSeconds(30);
 99
 100    /// <summary>
 101    /// Minimum interval between opportunistic prunes of expired channel rows. Pruning is housekeeping
 102    /// only (read queries filter on expiry), so throttling it keeps publishes off a full-table delete
 103    /// on every call. Set to <see cref="TimeSpan.Zero"/> to prune on every operation. Default: 30 seconds.
 104    /// </summary>
 3105    public TimeSpan PruneInterval { get; set; } = TimeSpan.FromSeconds(30);
 106
 107    /// <summary>Maximum attempts for a response-row insert. Set to 1 to disable publish retries. Default: 3.</summary>
 3108    public int PublishMaxAttempts { get; set; } = 3;
 109
 110    /// <summary>Initial delay before retrying a failed response-row insert. Default: 50 ms.</summary>
 3111    public TimeSpan PublishRetryBaseDelay { get; set; } = TimeSpan.FromMilliseconds(50);
 112
 113    /// <summary>Maximum delay between response-row insert retries. Default: 1 second.</summary>
 3114    public TimeSpan PublishRetryMaxDelay { get; set; } = TimeSpan.FromSeconds(1);
 115
 116    /// <summary>Validates the option values and throws on misconfiguration.</summary>
 117    public void Validate()
 118    {
 119        // Shared channel knobs (RecoveryStateExpiry, DefaultTimeout, DisposalDrainTimeout) go
 120        // through the ONE base guard set — a bespoke duplicate here silently missed every knob
 121        // added to the base later (DisposalDrainTimeout was validated nowhere on this provider).
 3122        ValidateShared(nameof(SqlServerAsyncResponseChannelOptions));
 123
 3124        if (string.IsNullOrWhiteSpace(ConnectionString))
 3125            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(ConnectionStrin
 126
 3127        SqlServerChannelSql.ValidateIdentifier(SchemaName, nameof(SchemaName));
 3128        SqlServerChannelSql.ValidateIdentifier(RecoveryStateTable, nameof(RecoveryStateTable));
 3129        SqlServerChannelSql.ValidateIdentifier(MessageTable, nameof(MessageTable));
 3130        SqlServerChannelSql.ValidateIdentifier(SubscriberTable, nameof(SubscriberTable));
 131
 3132        Positive(MessageRetention, nameof(MessageRetention));
 3133        Positive(DeliveryConfirmationTimeout, nameof(DeliveryConfirmationTimeout));
 3134        Positive(DeliveryConfirmationPollInterval, nameof(DeliveryConfirmationPollInterval));
 3135        Positive(ActivePollInterval, nameof(ActivePollInterval));
 3136        Positive(IdlePollInterval, nameof(IdlePollInterval));
 3137        Positive(SubscriberHeartbeatInterval, nameof(SubscriberHeartbeatInterval));
 3138        Positive(SubscriberHeartbeatTimeout, nameof(SubscriberHeartbeatTimeout));
 139
 3140        if (ActivePollInterval > IdlePollInterval)
 3141            throw new InvalidOperationException(
 3142                $"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(ActivePollInterval)} cannot exceed " +
 3143                $"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(IdlePollInterval)}; the idle interval is the ba
 144
 3145        if (MaxRemoteStackTraceLength < 0)
 3146            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(MaxRemoteStackT
 147
 3148        if (PendingMessageBatchSize <= 0)
 3149            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(PendingMessageB
 150
 3151        if (SubscriberHeartbeatInterval >= SubscriberHeartbeatTimeout)
 3152            throw new InvalidOperationException(
 3153                $"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(SubscriberHeartbeatInterval)} must be less than
 3154                $"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(SubscriberHeartbeatTimeout)}.");
 155
 3156        if (PruneInterval < TimeSpan.Zero)
 3157            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(PruneInterval)}
 158
 3159        if (PublishMaxAttempts <= 0)
 3160            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(PublishMaxAttem
 161
 3162        Positive(PublishRetryBaseDelay, nameof(PublishRetryBaseDelay));
 3163        Positive(PublishRetryMaxDelay, nameof(PublishRetryMaxDelay));
 3164        if (PublishRetryBaseDelay > PublishRetryMaxDelay)
 3165            throw new InvalidOperationException(
 3166                $"{nameof(SqlServerAsyncResponseChannelOptions)}.{nameof(PublishRetryBaseDelay)} cannot exceed {nameof(P
 3167    }
 168
 169    private static void Positive(TimeSpan value, string name)
 170    {
 3171        if (value <= TimeSpan.Zero)
 3172            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseChannelOptions)}.{name} must be positive
 3173    }
 174}