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

Information
Class: AsyncResponse.Transports.SQS.SqsOptionsValidator
Assembly: AsyncResponse.Transports.SQS
File(s): /_/src/Transports/AsyncResponse.Transports.SQS/SqsOptionsValidator.cs
Line coverage
100%
Covered lines: 93
Uncovered lines: 0
Coverable lines: 93
Total lines: 183
Line coverage: 100%
Branch coverage
96%
Covered branches: 64
Total branches: 66
Branch coverage: 96.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
ValidateCommon(...)94.44%3636100%
ValidateSubscriber(...)100%2828100%
Required(...)100%22100%

File(s)

/_/src/Transports/AsyncResponse.Transports.SQS/SqsOptionsValidator.cs

#LineLine coverage
 1namespace AsyncResponse.Transports.SQS;
 2
 3internal static class SqsOptionsValidator
 4{
 55    private static readonly TimeSpan MaxReceiveWaitTime = TimeSpan.FromSeconds(20);
 56    private static readonly TimeSpan MaxVisibilityTimeout = TimeSpan.FromHours(12);
 7
 8    public static void ValidateCommon(SqsAsyncResponseOptions options)
 9    {
 89810        Required(options.WorkerQueue, nameof(options.WorkerQueue));
 89811        Required(options.ResponseQueue, nameof(options.ResponseQueue));
 89812        Required(options.CorrelationIdAttribute, nameof(options.CorrelationIdAttribute));
 89813        Required(options.DefaultReplyTargetName, nameof(options.DefaultReplyTargetName));
 14
 89815        if (StringComparer.Ordinal.Equals(options.WorkerQueue, options.ResponseQueue))
 16        {
 217            throw new InvalidOperationException(
 218                $"{nameof(SqsAsyncResponseOptions)}.{nameof(options.WorkerQueue)} and " +
 219                $"{nameof(options.ResponseQueue)} must be distinct so worker and response subscribers do not consume eac
 20        }
 21
 89622        if (options.MaxMessagesPerReceive is < 1 or > 10)
 23        {
 424            throw new InvalidOperationException(
 425                $"{nameof(SqsAsyncResponseOptions)}.{nameof(options.MaxMessagesPerReceive)} must be between 1 and 10 (th
 26        }
 27
 89228        if (options.ReceiveWaitTime < TimeSpan.Zero || options.ReceiveWaitTime > MaxReceiveWaitTime)
 29        {
 430            throw new InvalidOperationException(
 431                $"{nameof(SqsAsyncResponseOptions)}.{nameof(options.ReceiveWaitTime)} must be between 0 and 20 seconds (
 32        }
 33
 88834        if (options.PublishMaxAttempts <= 0)
 235            throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{nameof(options.PublishMaxAttempts)}
 36
 88637        if (options.CreateQueues)
 38        {
 79839            Required(options.DeadLetterQueueSuffix, nameof(options.DeadLetterQueueSuffix));
 79640            if (options.MaxReceiveCount is < 1 or > 1000)
 41            {
 242                throw new InvalidOperationException(
 243                    $"{nameof(SqsAsyncResponseOptions)}.{nameof(options.MaxReceiveCount)} must be between 1 and 1000 (th
 44            }
 45
 46            // The derived dead-letter names must not collide with a LIVE queue (the guard every
 47            // sibling transport applies to its dead-letter destination): a redrive policy aimed at
 48            // the live response queue moves poison worker jobs into the ingress, where any
 49            // parseable JSON completes a real waiter — and provisioning would also silently
 50            // reconfigure the live queue's attributes.
 475251            foreach (var queue in new[] { options.WorkerQueue!, options.ResponseQueue! })
 52            {
 158453                if (SqsQueueAddress.IsUrl(queue))
 54                    continue;
 55
 158256                var deadLetterQueue = SqsQueueAddress.DeriveDeadLetterQueueName(queue, options.DeadLetterQueueSuffix!);
 158257                if (StringComparer.Ordinal.Equals(deadLetterQueue, options.WorkerQueue)
 158258                    || StringComparer.Ordinal.Equals(deadLetterQueue, options.ResponseQueue))
 59                {
 460                    throw new InvalidOperationException(
 461                        $"{nameof(SqsAsyncResponseOptions)}: the dead-letter queue derived for '{queue}' with " +
 462                        $"{nameof(options.DeadLetterQueueSuffix)} '{options.DeadLetterQueueSuffix}' is '{deadLetterQueue
 463                        "which collides with a live worker/response queue. Rename the queues or change the suffix.");
 64                }
 65            }
 66        }
 67
 87868        if (SqsQueueAddress.IsFifo(options.WorkerQueue))
 1269            Required(options.FifoMessageGroupIdFallback, nameof(options.FifoMessageGroupIdFallback));
 70
 87671        AsyncResponseChannelOptions.EnsureTimerBacked(options.PublishRetryBaseDelay, nameof(SqsAsyncResponseOptions), na
 87472        AsyncResponseChannelOptions.EnsureTimerBacked(options.PublishRetryMaxDelay, nameof(SqsAsyncResponseOptions), nam
 87473        AsyncResponseChannelOptions.EnsureTimerBacked(options.SubscriberRetryBaseDelay, nameof(SqsAsyncResponseOptions),
 87274        AsyncResponseChannelOptions.EnsureTimerBacked(options.SubscriberRetryMaxDelay, nameof(SqsAsyncResponseOptions), 
 87275        AsyncResponseChannelOptions.EnsureTimerBacked(options.ShutdownTimeout, nameof(SqsAsyncResponseOptions), nameof(o
 76
 87077        if (options.PublishRetryBaseDelay > options.PublishRetryMaxDelay)
 278            throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{nameof(options.PublishRetryBaseDela
 86879        if (options.SubscriberRetryBaseDelay > options.SubscriberRetryMaxDelay)
 280            throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{nameof(options.SubscriberRetryBaseD
 86681    }
 82
 83    public static void ValidateSubscriber(
 84        SqsAsyncResponseOptions transportOptions,
 85        SqsSubscriberOptions subscriberOptions,
 86        SqsSubscriberRole role)
 87    {
 89288        var optionPath = role is SqsSubscriberRole.Worker
 89289            ? $"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.WorkerSubscriber)}"
 89290            : $"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.ResponseSubscriber)}";
 91
 89292        if (subscriberOptions.VisibilityTimeout is { } visibilityTimeout
 89293            && (visibilityTimeout <= TimeSpan.Zero || visibilityTimeout > MaxVisibilityTimeout))
 94        {
 495            throw new InvalidOperationException(
 496                $"{optionPath}.{nameof(SqsSubscriberOptions.VisibilityTimeout)} must be positive and at most 12 hours (t
 97        }
 98
 88899        if (subscriberOptions.RedeliveryDelay is { } redeliveryDelay
 888100            && (redeliveryDelay < TimeSpan.Zero || redeliveryDelay > MaxVisibilityTimeout))
 101        {
 4102            throw new InvalidOperationException(
 4103                $"{optionPath}.{nameof(SqsSubscriberOptions.RedeliveryDelay)} must be between zero and 12 hours (the SQS
 104        }
 105
 884106        if (subscriberOptions.VisibilityRenewalInterval is { } renewalInterval)
 107        {
 108            // The renewal heartbeat arms Task.Delay, so the interval carries the timer ceiling
 109            // (in practice the shorter-than-visibility rule below is far tighter).
 32110            AsyncResponseChannelOptions.EnsureTimerBacked(renewalInterval, optionPath, nameof(SqsSubscriberOptions.Visib
 111
 30112            if (subscriberOptions.VisibilityTimeout is not { } renewedVisibility)
 113            {
 2114                throw new InvalidOperationException(
 2115                    $"{optionPath}.{nameof(SqsSubscriberOptions.VisibilityRenewalInterval)} requires " +
 2116                    $"{nameof(SqsSubscriberOptions.VisibilityTimeout)} so the heartbeat knows how far to extend each mes
 117            }
 118
 28119            if (renewalInterval >= renewedVisibility)
 120            {
 2121                throw new InvalidOperationException(
 2122                    $"{optionPath}.{nameof(SqsSubscriberOptions.VisibilityRenewalInterval)} must be shorter than " +
 2123                    $"{nameof(SqsSubscriberOptions.VisibilityTimeout)}, or messages become visible between heartbeats.")
 124            }
 125        }
 126
 878127        switch (subscriberOptions.AckMode)
 128        {
 129            case SqsAckMode.AckAfterHandlerCompletes:
 130                // ShutdownTimeout is spent at shutdown even without a background drain: the
 131                // visibility-renewal join on the final batch waits it out against a degraded
 132                // endpoint, exactly as its own XML doc says ("at shutdown it counts against the
 133                // host's budget").
 832134                ShutdownBudgetValidator.Validate(
 832135                    "SQS",
 832136                    $"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.HostShutdownTimeout)}",
 832137                    transportOptions.HostShutdownTimeout,
 832138                    ($"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.ShutdownTimeout)}", transportOp
 830139                return;
 140
 141            case SqsAckMode.AckAfterEnqueue:
 44142                if (subscriberOptions.BackgroundWorkerCount <= 0)
 143                {
 4144                    throw new InvalidOperationException(
 4145                        $"{optionPath}.{nameof(SqsSubscriberOptions.BackgroundWorkerCount)} must be explicitly configure
 4146                        $"when {nameof(SqsSubscriberOptions.AckMode)} is {nameof(SqsAckMode.AckAfterEnqueue)}.");
 147                }
 148
 40149                if (subscriberOptions.BackgroundQueueCapacity <= 0)
 150                {
 2151                    throw new InvalidOperationException(
 2152                        $"{optionPath}.{nameof(SqsSubscriberOptions.BackgroundQueueCapacity)} must be explicitly configu
 2153                        $"when {nameof(SqsSubscriberOptions.AckMode)} is {nameof(SqsAckMode.AckAfterEnqueue)}.");
 154                }
 155
 38156                AsyncResponseChannelOptions.EnsureTimerBacked(subscriberOptions.BackgroundDrainTimeout, optionPath, name
 157
 158                // The receive loop stops with the host token and the SDK client needs no bounded
 159                // close, but shutdown spends BOTH the background drain and the visibility-renewal
 160                // join's ShutdownTimeout (Azure Service Bus parity) — validating only the drain let
 161                // a raised ShutdownTimeout blow the host budget and force-terminate mid-join,
 162                // redelivering handled-but-undeleted work.
 36163                ShutdownBudgetValidator.Validate(
 36164                    "SQS",
 36165                    $"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.HostShutdownTimeout)}",
 36166                    transportOptions.HostShutdownTimeout,
 36167                    ($"{optionPath}.{nameof(SqsSubscriberOptions.BackgroundDrainTimeout)}", subscriberOptions.Background
 36168                    ($"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.ShutdownTimeout)}", transportOp
 169
 30170                return;
 171
 172            default:
 2173                throw new InvalidOperationException(
 2174                    $"{optionPath}.{nameof(SqsSubscriberOptions.AckMode)} has unsupported value '{subscriberOptions.AckM
 175        }
 176    }
 177
 178    public static string Required(string? value, string name)
 5230179        => !string.IsNullOrWhiteSpace(value)
 5230180            ? value
 5230181            : throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{name} must be configured.");
 182
 183}