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

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

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Channels/AsyncResponse.Channels.PostgreSQL/PostgreSqlAsyncResponseChannelOptions.cs

#LineLine coverage
 1namespace AsyncResponse.Channels.PostgreSQL;
 2
 3/// <summary>
 4/// Options for the PostgreSQL-backed async-response channel.
 5/// <para>
 6/// Active waiters are woken with PostgreSQL <c>LISTEN/NOTIFY</c>. Response envelopes are stored in a
 7/// table and notifications carry only the message id, avoiding PostgreSQL's small NOTIFY payload
 8/// limit. Durable <see cref="RecoveryState"/> entries live in a separate table so late responses can
 9/// resume or fail flows after the original waiter process dies.
 10/// </para>
 11/// </summary>
 12public sealed class PostgreSqlAsyncResponseChannelOptions : DurableAsyncResponseChannelOptions
 13{
 14    /// <summary>The channel name reported to the startup validator.</summary>
 15    public const string ChannelName = "PostgreSQL";
 16
 17    /// <summary>Database schema that contains the channel tables. Default: <c>public</c>.</summary>
 318    public string SchemaName { get; set; } = "public";
 19
 20    /// <summary>
 21    /// Table storing durable recovery registrations. Each waiter registration is one row keyed by
 22    /// correlation id and registration id.
 23    /// </summary>
 324    public string RecoveryStateTable { get; set; } = "asyncresponse_recovery_state";
 25
 26    /// <summary>
 27    /// Table storing response envelopes until they expire. Notifications carry message ids; waiters
 28    /// load the envelope from this table.
 29    /// </summary>
 330    public string MessageTable { get; set; } = "asyncresponse_channel_messages";
 31
 32    /// <summary>
 33    /// Table storing short-lived live-subscriber heartbeats for watchdog liveness and the publish
 34    /// fast path.
 35    /// </summary>
 336    public string SubscriberTable { get; set; } = "asyncresponse_channel_subscribers";
 37
 38    /// <summary>
 39    /// PostgreSQL notification channel used to wake local listener loops. Must be a simple
 40    /// PostgreSQL identifier. Default: <c>asyncresponse_channel_notify</c>.
 41    /// </summary>
 342    public string NotificationChannel { get; set; } = "asyncresponse_channel_notify";
 43
 44    /// <summary>
 45    /// Creates the schema, tables, and indexes on first use. Disable when migrations provision them
 46    /// out of band.
 47    /// </summary>
 348    public bool AutoCreateSchema { get; set; } = true;
 49
 50    /// <summary>
 51    /// How long response-envelope rows are retained for active waiter delivery and missed-notify
 52    /// recovery. Expired rows are pruned opportunistically during channel operations.
 53    /// </summary>
 354    public TimeSpan MessageRetention { get; set; } = TimeSpan.FromHours(1);
 55
 56    /// <summary>
 57    /// How long a publisher waits for a live waiter to acknowledge loading a response envelope
 58    /// before treating the response as lost-subscriber delivery. Default: 5 seconds.
 59    /// </summary>
 360    public TimeSpan DeliveryConfirmationTimeout { get; set; } = TimeSpan.FromSeconds(5);
 61
 62    /// <summary>
 63    /// Poll interval used while a publisher waits for delivery acknowledgement. Default: 50 ms.
 64    /// </summary>
 365    public TimeSpan DeliveryConfirmationPollInterval { get; set; } = TimeSpan.FromMilliseconds(50);
 66
 67    /// <summary>
 68    /// Fallback poll interval used by the listener loop to catch messages if a notification is
 69    /// missed during reconnect. Default: 250 ms.
 70    /// </summary>
 371    public TimeSpan ListenerPollInterval { get; set; } = TimeSpan.FromMilliseconds(250);
 72
 73    /// <summary>
 74    /// Number of pending response messages loaded per subscribed correlation id per listener pass.
 75    /// Default: 64.
 76    /// </summary>
 377    public int PendingMessageBatchSize { get; set; } = 64;
 78
 79    /// <summary>
 80    /// How often a live waiter refreshes its subscriber heartbeat row. Default: 10 seconds.
 81    /// </summary>
 382    public TimeSpan SubscriberHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
 83
 84    /// <summary>
 85    /// How long a subscriber heartbeat remains live without refresh. Keep this above
 86    /// <see cref="SubscriberHeartbeatInterval"/>. Default: 30 seconds.
 87    /// </summary>
 388    public TimeSpan SubscriberHeartbeatTimeout { get; set; } = TimeSpan.FromSeconds(30);
 89
 90    /// <summary>
 91    /// Minimum interval between opportunistic prunes of expired channel rows. Pruning is housekeeping
 92    /// only (read queries filter on expiry), so throttling it keeps publishes off a full-table delete
 93    /// on every call. Set to <see cref="TimeSpan.Zero"/> to prune on every operation. Default: 30 seconds.
 94    /// </summary>
 395    public TimeSpan PruneInterval { get; set; } = TimeSpan.FromSeconds(30);
 96
 97    /// <summary>Maximum attempts for a response-row insert. Set to 1 to disable publish retries. Default: 3.</summary>
 398    public int PublishMaxAttempts { get; set; } = 3;
 99
 100    /// <summary>Initial delay before retrying a failed response-row insert. Default: 50 ms.</summary>
 3101    public TimeSpan PublishRetryBaseDelay { get; set; } = TimeSpan.FromMilliseconds(50);
 102
 103    /// <summary>Maximum delay between response-row insert retries. Default: 1 second.</summary>
 3104    public TimeSpan PublishRetryMaxDelay { get; set; } = TimeSpan.FromSeconds(1);
 105
 106    /// <summary>Validates the option values and throws on misconfiguration.</summary>
 107    public void Validate()
 108    {
 109        // Shared channel knobs (RecoveryStateExpiry, DefaultTimeout, DisposalDrainTimeout) go
 110        // through the ONE base guard set — a bespoke duplicate here silently missed every knob
 111        // added to the base later (DisposalDrainTimeout was validated nowhere on this provider).
 3112        ValidateShared(nameof(PostgreSqlAsyncResponseChannelOptions));
 113
 3114        PostgreSqlChannelSql.ValidateIdentifier(SchemaName, nameof(SchemaName));
 3115        PostgreSqlChannelSql.ValidateIdentifier(RecoveryStateTable, nameof(RecoveryStateTable));
 3116        PostgreSqlChannelSql.ValidateIdentifier(MessageTable, nameof(MessageTable));
 3117        PostgreSqlChannelSql.ValidateIdentifier(SubscriberTable, nameof(SubscriberTable));
 3118        PostgreSqlChannelSql.ValidateIdentifier(NotificationChannel, nameof(NotificationChannel));
 119
 3120        Positive(MessageRetention, nameof(MessageRetention));
 3121        Positive(DeliveryConfirmationTimeout, nameof(DeliveryConfirmationTimeout));
 3122        Positive(DeliveryConfirmationPollInterval, nameof(DeliveryConfirmationPollInterval));
 3123        Positive(ListenerPollInterval, nameof(ListenerPollInterval));
 3124        Positive(SubscriberHeartbeatInterval, nameof(SubscriberHeartbeatInterval));
 3125        Positive(SubscriberHeartbeatTimeout, nameof(SubscriberHeartbeatTimeout));
 126
 3127        if (MaxRemoteStackTraceLength < 0)
 3128            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(MaxRemoteStack
 129
 3130        if (PendingMessageBatchSize <= 0)
 3131            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(PendingMessage
 132
 3133        if (SubscriberHeartbeatInterval >= SubscriberHeartbeatTimeout)
 3134            throw new InvalidOperationException(
 3135                $"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(SubscriberHeartbeatInterval)} must be less tha
 3136                $"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(SubscriberHeartbeatTimeout)}.");
 137
 3138        if (PruneInterval < TimeSpan.Zero)
 3139            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(PruneInterval)
 140
 3141        if (PublishMaxAttempts <= 0)
 3142            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(PublishMaxAtte
 143
 3144        Positive(PublishRetryBaseDelay, nameof(PublishRetryBaseDelay));
 3145        Positive(PublishRetryMaxDelay, nameof(PublishRetryMaxDelay));
 3146        if (PublishRetryBaseDelay > PublishRetryMaxDelay)
 3147            throw new InvalidOperationException(
 3148                $"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(PublishRetryBaseDelay)} cannot exceed {nameof(
 3149    }
 150
 151    private static void Positive(TimeSpan value, string name)
 152    {
 3153        if (value <= TimeSpan.Zero)
 3154            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{name} must be positiv
 3155    }
 156}