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

Information
Class: AsyncResponse.Channels.PostgreSQL.PostgreSqlAsyncResponseChannelOptions
Assembly: AsyncResponse.Channels.PostgreSQL
File(s): /_/src/Channels/AsyncResponse.Channels.PostgreSQL/PostgreSqlAsyncResponseChannelOptions.cs
Line coverage
100%
Covered lines: 59
Uncovered lines: 0
Coverable lines: 59
Total lines: 183
Line coverage: 100%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/_/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>
 388218    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>
 561924    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>
 906230    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>
 523136    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>
 489942    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>
 100448    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>
 424754    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>
 340660    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>
 313765    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>
 1051771    public TimeSpan ListenerPollInterval { get; set; } = TimeSpan.FromMilliseconds(250);
 72
 73    /// <summary>
 74    /// Minimum interval between full safety-net sweeps. A full sweep queries the store once per
 75    /// subscribed correlation id, so its idle cost is W queries per <see cref="ListenerPollInterval"/>
 76    /// tick with W in-flight waiters. On this provider the sweep only covers wake notifications
 77    /// lost in failure windows (NOTIFY carries normal delivery), so this bounds idle
 78    /// database load without touching normal delivery latency — it stretches only the worst-case
 79    /// recovery of a LOST wake. Default: 5 seconds (an unbounded null swept every waiter on every
 80    /// 250 ms tick — W sequential queries per tick of pure idle load). Set null to sweep on every
 81    /// poll tick.
 82    /// </summary>
 678483    public TimeSpan? FullSweepInterval { get; set; } = TimeSpan.FromSeconds(5);
 84
 85    /// <summary>
 86    /// Number of pending response messages loaded per subscribed correlation id per listener pass.
 87    /// Default: 64.
 88    /// </summary>
 342689    public int PendingMessageBatchSize { get; set; } = 64;
 90
 91    /// <summary>
 92    /// Minimum interval between incremental reconciliation passes over retained message history.
 93    /// Normal scans keep their forward cursor; reconciliation catches late commits behind it,
 94    /// including rows already acknowledged by another process. One history page is read per
 95    /// dispatch pass so a long history cannot monopolize delivery to other correlations.
 96    /// Default: 5 seconds; large histories take additional poll intervals to reconcile.
 97    /// </summary>
 181598    public TimeSpan HistoryReconciliationInterval { get; set; } = TimeSpan.FromSeconds(5);
 99
 100
 101    /// <summary>
 102    /// How often a live waiter refreshes its subscriber heartbeat row. Default: 10 seconds.
 103    /// </summary>
 5463104    public TimeSpan SubscriberHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
 105
 106    /// <summary>
 107    /// How long a subscriber heartbeat remains live without refresh. Keep this above
 108    /// <see cref="SubscriberHeartbeatInterval"/>. Default: 30 seconds.
 109    /// </summary>
 3227110    public TimeSpan SubscriberHeartbeatTimeout { get; set; } = TimeSpan.FromSeconds(30);
 111
 112    /// <summary>
 113    /// Minimum interval between opportunistic prunes of expired channel rows. Pruning is housekeeping
 114    /// only (read queries filter on expiry), so throttling it keeps publishes off a full-table delete
 115    /// on every call. Set to <see cref="TimeSpan.Zero"/> to prune on every operation. Default: 30 seconds.
 116    /// </summary>
 3417117    public TimeSpan PruneInterval { get; set; } = TimeSpan.FromSeconds(30);
 118
 119    /// <summary>Maximum attempts for a response-row insert. Set to 1 to disable publish retries. Default: 3.</summary>
 1992120    public int PublishMaxAttempts { get; set; } = 3;
 121
 122    /// <summary>Initial delay before retrying a failed response-row insert. Default: 50 ms.</summary>
 2858123    public TimeSpan PublishRetryBaseDelay { get; set; } = TimeSpan.FromMilliseconds(50);
 124
 125    /// <summary>Maximum delay between response-row insert retries. Default: 1 second.</summary>
 2860126    public TimeSpan PublishRetryMaxDelay { get; set; } = TimeSpan.FromSeconds(1);
 127
 128    /// <summary>Validates the option values and throws on misconfiguration.</summary>
 129    public void Validate()
 130    {
 131        // Shared channel knobs (RecoveryStateExpiry, DefaultTimeout, DisposalDrainTimeout) go
 132        // through the ONE base guard set — a bespoke duplicate here silently missed every knob
 133        // added to the base later (DisposalDrainTimeout was validated nowhere on this provider).
 924134        ValidateShared(nameof(PostgreSqlAsyncResponseChannelOptions));
 135
 918136        PostgreSqlChannelSql.ValidateIdentifier(SchemaName, nameof(SchemaName));
 912137        PostgreSqlChannelSql.ValidateIdentifier(RecoveryStateTable, nameof(RecoveryStateTable));
 912138        PostgreSqlChannelSql.ValidateIdentifier(MessageTable, nameof(MessageTable));
 908139        PostgreSqlChannelSql.ValidateIdentifier(SubscriberTable, nameof(SubscriberTable));
 908140        PostgreSqlChannelSql.ValidateIdentifier(NotificationChannel, nameof(NotificationChannel));
 908141        PostgreSqlChannelSql.ValidateNamePlan(this);
 142
 902143        EnsurePersistedTtl(MessageRetention, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(MessageRetention));
 898144        EnsurePersistedTtl(DeliveryConfirmationTimeout, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(DeliveryCo
 894145        if (MessageRetention <= DeliveryConfirmationTimeout)
 2146            throw new InvalidOperationException(
 2147                $"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(MessageRetention)} must exceed " +
 2148                $"{nameof(DeliveryConfirmationTimeout)}: a message row pruned inside the confirmation window is " +
 2149                "indistinguishable from an acknowledged one, so the response would be reported delivered and " +
 2150                "lost-response recovery silently skipped.");
 892151        EnsureTimerBacked(DeliveryConfirmationPollInterval, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(Delive
 888152        EnsureTimerBacked(ListenerPollInterval, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(ListenerPollInterv
 884153        EnsureTimerBacked(HistoryReconciliationInterval, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(HistoryRe
 882154        if (FullSweepInterval is { } fullSweepInterval)
 818155            EnsureTimerBacked(fullSweepInterval, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(FullSweepInterval
 882156        EnsureTimerBacked(SubscriberHeartbeatInterval, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(SubscriberH
 882157        EnsurePersistedTtl(SubscriberHeartbeatTimeout, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(SubscriberH
 158
 880159        if (MaxRemoteStackTraceLength < 0)
 2160            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(MaxRemoteStack
 161
 878162        if (PendingMessageBatchSize <= 0)
 2163            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(PendingMessage
 164
 876165        if (SubscriberHeartbeatInterval >= SubscriberHeartbeatTimeout)
 2166            throw new InvalidOperationException(
 2167                $"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(SubscriberHeartbeatInterval)} must be less tha
 2168                $"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(SubscriberHeartbeatTimeout)}.");
 169
 874170        if (PruneInterval < TimeSpan.Zero)
 2171            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(PruneInterval)
 172
 872173        if (PublishMaxAttempts <= 0)
 2174            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(PublishMaxAtte
 175
 870176        EnsureTimerBacked(PublishRetryBaseDelay, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(PublishRetryBaseD
 870177        EnsureTimerBacked(PublishRetryMaxDelay, nameof(PostgreSqlAsyncResponseChannelOptions), nameof(PublishRetryMaxDel
 868178        if (PublishRetryBaseDelay > PublishRetryMaxDelay)
 2179            throw new InvalidOperationException(
 2180                $"{nameof(PostgreSqlAsyncResponseChannelOptions)}.{nameof(PublishRetryBaseDelay)} cannot exceed {nameof(
 866181    }
 182
 183}