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

Information
Class: AsyncResponse.Transports.PostgreSQL.PostgreSqlTransportOptionsValidator
Assembly: AsyncResponse.Transports.PostgreSQL
File(s): /_/src/Transports/AsyncResponse.Transports.PostgreSQL/PostgreSqlTransportOptionsValidator.cs
Line coverage
100%
Covered lines: 82
Uncovered lines: 0
Coverable lines: 82
Total lines: 143
Line coverage: 100%
Branch coverage
95%
Covered branches: 46
Total branches: 48
Branch coverage: 95.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ValidateCommon(...)92.85%1414100%
ValidateSubscriber(...)100%22100%
ValidateSubscriber(...)100%1212100%
Required(...)100%22100%
ValidateIdentifier(...)100%66100%
IsIdentifier(...)91.66%1212100%

File(s)

/_/src/Transports/AsyncResponse.Transports.PostgreSQL/PostgreSqlTransportOptionsValidator.cs

#LineLine coverage
 1using AsyncResponse.Internal;
 2
 3namespace AsyncResponse.Transports.PostgreSQL;
 4
 5internal static class PostgreSqlTransportOptionsValidator
 6{
 7    public static void ValidateCommon(PostgreSqlAsyncResponseTransportOptions options)
 8    {
 8729        ValidateIdentifier(options.SchemaName, nameof(options.SchemaName));
 86810        ValidateIdentifier(options.MessageTable, nameof(options.MessageTable));
 86611        ValidateIdentifier(options.NotificationChannel, nameof(options.NotificationChannel));
 86412        Required(options.WorkerQueue, nameof(options.WorkerQueue));
 86413        Required(options.ResponseQueue, nameof(options.ResponseQueue));
 86414        Required(options.DeadLetterQueue, nameof(options.DeadLetterQueue));
 86415        Required(options.CorrelationIdHeader, nameof(options.CorrelationIdHeader));
 86216        Required(options.DefaultReplyTargetName, nameof(options.DefaultReplyTargetName));
 17
 18        // All three logical queues share one table, distinguished only by the queue column. Equal
 19        // names would make subscribers consume each other's rows (or re-consume dead letters).
 86220        if (StringComparer.Ordinal.Equals(options.WorkerQueue, options.ResponseQueue)
 86221            || StringComparer.Ordinal.Equals(options.WorkerQueue, options.DeadLetterQueue)
 86222            || StringComparer.Ordinal.Equals(options.ResponseQueue, options.DeadLetterQueue))
 23        {
 224            throw new InvalidOperationException(
 225                $"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{nameof(options.WorkerQueue)}, " +
 226                $"{nameof(options.ResponseQueue)}, and {nameof(options.DeadLetterQueue)} must be distinct; they share on
 27        }
 28
 29        // The queue table's derived index names reserve suffix space, but a table whose name ends
 30        // exactly where a reserved stem truncates can still derive its own name, silently skipping
 31        // index creation (indexes and tables share PostgreSQL's relation namespace).
 86032        (string Role, string Name)[] namePlan =
 86033        [
 86034            ($"{nameof(options.MessageTable)} table", options.MessageTable),
 86035            ("dequeue index (derived from MessageTable)", PostgreSqlTransportStore.IndexName(options.MessageTable, "read
 86036            ("created index (derived from MessageTable)", PostgreSqlTransportStore.IndexName(options.MessageTable, "crea
 86037        ];
 86038        RelationalNamePlan.RequireDistinct(
 86039            namePlan,
 86040            nameof(PostgreSqlAsyncResponseTransportOptions),
 86041            $"; rename {nameof(options.MessageTable)} so the derived index names stay distinct.");
 42
 43        // Timer-armed knobs get the .NET timer ceiling (LockTimeout also drives the in-process
 44        // lease-renewal Task.Delay at a third of its value); DeadLetterRetention and RedeliveryDelay are
 45        // database-side "now + value" stamps and get the persistence bound instead.
 85646        if (options.DeadLetterRetention is { } deadLetterRetention)
 2647            AsyncResponseChannelOptions.EnsurePersistedTtl(deadLetterRetention, nameof(PostgreSqlAsyncResponseTransportO
 48
 85249        AsyncResponseChannelOptions.EnsureTimerBacked(options.LockTimeout, nameof(PostgreSqlAsyncResponseTransportOption
 84850        AsyncResponseChannelOptions.EnsureTimerBacked(options.PublishRetryBaseDelay, nameof(PostgreSqlAsyncResponseTrans
 84851        AsyncResponseChannelOptions.EnsureTimerBacked(options.PublishRetryMaxDelay, nameof(PostgreSqlAsyncResponseTransp
 84852        AsyncResponseChannelOptions.EnsureTimerBacked(options.SubscriberRetryBaseDelay, nameof(PostgreSqlAsyncResponseTr
 84853        AsyncResponseChannelOptions.EnsureTimerBacked(options.SubscriberRetryMaxDelay, nameof(PostgreSqlAsyncResponseTra
 84854        AsyncResponseChannelOptions.EnsureTimerBacked(options.ShutdownTimeout, nameof(PostgreSqlAsyncResponseTransportOp
 55
 84856        if (options.PublishMaxAttempts <= 0)
 257            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{nameof(options.Publ
 84658        if (options.PublishRetryBaseDelay > options.PublishRetryMaxDelay)
 259            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{nameof(options.Publ
 84460        if (options.SubscriberRetryBaseDelay > options.SubscriberRetryMaxDelay)
 261            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{nameof(options.Subs
 84262    }
 63
 64    /// <summary>Validates the supplied subscriber options together with the transport-wide shutdown budget.</summary>
 65    public static void ValidateSubscriber(
 66        PostgreSqlAsyncResponseTransportOptions transportOptions,
 67        PostgreSqlSubscriberOptions subscriber,
 68        string role)
 69    {
 87870        ValidateSubscriber(subscriber, role);
 71
 87472        if (subscriber.AckMode is not PostgreSqlAckMode.AckAfterEnqueue)
 82473            return;
 74
 75        // PostgreSQL spends the background drain plus the LISTEN-task join (ShutdownTimeout)
 76        // at shutdown; both must fit inside the host budget or ACKed work is truncated.
 5077        ShutdownBudgetValidator.Validate(
 5078            "PostgreSQL",
 5079            $"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{nameof(transportOptions.HostShutdownTimeout)}",
 5080            transportOptions.HostShutdownTimeout,
 5081            ($"{nameof(PostgreSqlSubscriberOptions)}.{nameof(subscriber.BackgroundDrainTimeout)} ({role})", subscriber.B
 5082            ($"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{nameof(transportOptions.ShutdownTimeout)}", transportO
 4683    }
 84
 85    public static void ValidateSubscriber(PostgreSqlSubscriberOptions subscriber, string role)
 86    {
 89487        if (subscriber.BatchSize <= 0)
 288            throw new InvalidOperationException($"{nameof(PostgreSqlSubscriberOptions)}.{nameof(subscriber.BatchSize)} (
 89289        if (subscriber.MaxDeliveryAttempts < 0)
 290            throw new InvalidOperationException($"{nameof(PostgreSqlSubscriberOptions)}.{nameof(subscriber.MaxDeliveryAt
 91
 92        // RedeliveryDelay is a database-side "now + delay" visibility stamp (persistence bound);
 93        // EmptyPollDelay arms the idle-poll Task.Delay (timer ceiling).
 89094        AsyncResponseChannelOptions.EnsurePersistedTtl(subscriber.RedeliveryDelay, nameof(PostgreSqlSubscriberOptions), 
 88895        AsyncResponseChannelOptions.EnsureTimerBacked(subscriber.EmptyPollDelay, nameof(PostgreSqlSubscriberOptions), $"
 96
 88897        switch (subscriber.AckMode)
 98        {
 99            case PostgreSqlAckMode.AckAfterHandlerCompletes:
 826100                return;
 101            case PostgreSqlAckMode.AckAfterEnqueue:
 60102                if (subscriber.BackgroundWorkerCount <= 0)
 6103                    throw new InvalidOperationException($"{nameof(PostgreSqlSubscriberOptions)}.{nameof(subscriber.Backg
 54104                if (subscriber.BackgroundQueueCapacity <= 0)
 2105                    throw new InvalidOperationException($"{nameof(PostgreSqlSubscriberOptions)}.{nameof(subscriber.Backg
 52106                AsyncResponseChannelOptions.EnsureTimerBacked(subscriber.BackgroundDrainTimeout, nameof(PostgreSqlSubscr
 52107                return;
 108            default:
 2109                throw new InvalidOperationException($"{nameof(PostgreSqlSubscriberOptions)}.{nameof(subscriber.AckMode)}
 110        }
 111    }
 112
 113    public static string Required(string? value, string name)
 4353114        => !string.IsNullOrWhiteSpace(value)
 4353115            ? value
 4353116            : throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{name} must be con
 117
 118    public static void ValidateIdentifier(string? value, string name)
 119    {
 2606120        if (string.IsNullOrWhiteSpace(value))
 2121            throw new InvalidOperationException($"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{name} must be confi
 2604122        if (!IsIdentifier(value))
 4123            throw new InvalidOperationException(
 4124                $"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{name} '{value}' must be a simple PostgreSQL identif
 125        // PostgreSQL TRUNCATES over-limit identifiers silently (a NOTICE, not an error), so an
 126        // over-limit configured name would create/address an object under a different name.
 2600127        if (value.Length > 63)
 2128            throw new InvalidOperationException(
 2129                $"{nameof(PostgreSqlAsyncResponseTransportOptions)}.{name} '{value}' is {value.Length} characters; Postg
 2598130    }
 131
 132    private static bool IsIdentifier(string value)
 133    {
 2604134        if (value.Length == 0 || !(char.IsAsciiLetter(value[0]) || value[0] == '_'))
 2135            return false;
 127438136        foreach (var c in value)
 137        {
 61118138            if (!(char.IsAsciiLetterOrDigit(c) || c == '_'))
 2139                return false;
 140        }
 2600141        return true;
 142    }
 143}