| | | 1 | | namespace AsyncResponse.Transports.SQS; |
| | | 2 | | |
| | | 3 | | internal static class SqsOptionsValidator |
| | | 4 | | { |
| | 3 | 5 | | private static readonly TimeSpan MaxReceiveWaitTime = TimeSpan.FromSeconds(20); |
| | 3 | 6 | | private static readonly TimeSpan MaxVisibilityTimeout = TimeSpan.FromHours(12); |
| | | 7 | | |
| | | 8 | | public static void ValidateCommon(SqsAsyncResponseOptions options) |
| | | 9 | | { |
| | 3 | 10 | | Required(options.WorkerQueue, nameof(options.WorkerQueue)); |
| | 3 | 11 | | Required(options.ResponseQueue, nameof(options.ResponseQueue)); |
| | 3 | 12 | | Required(options.CorrelationIdAttribute, nameof(options.CorrelationIdAttribute)); |
| | 3 | 13 | | Required(options.DefaultReplyTargetName, nameof(options.DefaultReplyTargetName)); |
| | | 14 | | |
| | 3 | 15 | | if (StringComparer.Ordinal.Equals(options.WorkerQueue, options.ResponseQueue)) |
| | | 16 | | { |
| | 2 | 17 | | throw new InvalidOperationException( |
| | 2 | 18 | | $"{nameof(SqsAsyncResponseOptions)}.{nameof(options.WorkerQueue)} and " + |
| | 2 | 19 | | $"{nameof(options.ResponseQueue)} must be distinct so worker and response subscribers do not consume eac |
| | | 20 | | } |
| | | 21 | | |
| | 3 | 22 | | if (options.MaxMessagesPerReceive is < 1 or > 10) |
| | | 23 | | { |
| | 2 | 24 | | throw new InvalidOperationException( |
| | 2 | 25 | | $"{nameof(SqsAsyncResponseOptions)}.{nameof(options.MaxMessagesPerReceive)} must be between 1 and 10 (th |
| | | 26 | | } |
| | | 27 | | |
| | 3 | 28 | | if (options.ReceiveWaitTime < TimeSpan.Zero || options.ReceiveWaitTime > MaxReceiveWaitTime) |
| | | 29 | | { |
| | 2 | 30 | | throw new InvalidOperationException( |
| | 2 | 31 | | $"{nameof(SqsAsyncResponseOptions)}.{nameof(options.ReceiveWaitTime)} must be between 0 and 20 seconds ( |
| | | 32 | | } |
| | | 33 | | |
| | 3 | 34 | | if (options.PublishMaxAttempts <= 0) |
| | 2 | 35 | | throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{nameof(options.PublishMaxAttempts)} |
| | | 36 | | |
| | 3 | 37 | | if (options.CreateQueues) |
| | | 38 | | { |
| | 3 | 39 | | Required(options.DeadLetterQueueSuffix, nameof(options.DeadLetterQueueSuffix)); |
| | 3 | 40 | | if (options.MaxReceiveCount is < 1 or > 1000) |
| | | 41 | | { |
| | 2 | 42 | | throw new InvalidOperationException( |
| | 2 | 43 | | $"{nameof(SqsAsyncResponseOptions)}.{nameof(options.MaxReceiveCount)} must be between 1 and 1000 (th |
| | | 44 | | } |
| | | 45 | | } |
| | | 46 | | |
| | 3 | 47 | | if (SqsQueueAddress.IsFifo(options.WorkerQueue)) |
| | 2 | 48 | | Required(options.FifoMessageGroupIdFallback, nameof(options.FifoMessageGroupIdFallback)); |
| | | 49 | | |
| | 3 | 50 | | Positive(options.PublishRetryBaseDelay, nameof(options.PublishRetryBaseDelay)); |
| | 3 | 51 | | Positive(options.PublishRetryMaxDelay, nameof(options.PublishRetryMaxDelay)); |
| | 3 | 52 | | Positive(options.SubscriberRetryBaseDelay, nameof(options.SubscriberRetryBaseDelay)); |
| | 3 | 53 | | Positive(options.SubscriberRetryMaxDelay, nameof(options.SubscriberRetryMaxDelay)); |
| | | 54 | | |
| | 3 | 55 | | if (options.PublishRetryBaseDelay > options.PublishRetryMaxDelay) |
| | 2 | 56 | | throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{nameof(options.PublishRetryBaseDela |
| | 3 | 57 | | if (options.SubscriberRetryBaseDelay > options.SubscriberRetryMaxDelay) |
| | 2 | 58 | | throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{nameof(options.SubscriberRetryBaseD |
| | 3 | 59 | | } |
| | | 60 | | |
| | | 61 | | public static void ValidateSubscriber( |
| | | 62 | | SqsAsyncResponseOptions transportOptions, |
| | | 63 | | SqsSubscriberOptions subscriberOptions, |
| | | 64 | | SqsSubscriberRole role) |
| | | 65 | | { |
| | 3 | 66 | | var optionPath = role is SqsSubscriberRole.Worker |
| | 3 | 67 | | ? $"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.WorkerSubscriber)}" |
| | 3 | 68 | | : $"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.ResponseSubscriber)}"; |
| | | 69 | | |
| | 3 | 70 | | if (subscriberOptions.VisibilityTimeout is { } visibilityTimeout |
| | 3 | 71 | | && (visibilityTimeout <= TimeSpan.Zero || visibilityTimeout > MaxVisibilityTimeout)) |
| | | 72 | | { |
| | 2 | 73 | | throw new InvalidOperationException( |
| | 2 | 74 | | $"{optionPath}.{nameof(SqsSubscriberOptions.VisibilityTimeout)} must be positive and at most 12 hours (t |
| | | 75 | | } |
| | | 76 | | |
| | 3 | 77 | | if (subscriberOptions.RedeliveryDelay is { } redeliveryDelay |
| | 3 | 78 | | && (redeliveryDelay < TimeSpan.Zero || redeliveryDelay > MaxVisibilityTimeout)) |
| | | 79 | | { |
| | 2 | 80 | | throw new InvalidOperationException( |
| | 2 | 81 | | $"{optionPath}.{nameof(SqsSubscriberOptions.RedeliveryDelay)} must be between zero and 12 hours (the SQS |
| | | 82 | | } |
| | | 83 | | |
| | 3 | 84 | | if (subscriberOptions.VisibilityRenewalInterval is { } renewalInterval) |
| | | 85 | | { |
| | 2 | 86 | | if (renewalInterval <= TimeSpan.Zero) |
| | | 87 | | { |
| | 2 | 88 | | throw new InvalidOperationException( |
| | 2 | 89 | | $"{optionPath}.{nameof(SqsSubscriberOptions.VisibilityRenewalInterval)} must be positive when set.") |
| | | 90 | | } |
| | | 91 | | |
| | 2 | 92 | | if (subscriberOptions.VisibilityTimeout is not { } renewedVisibility) |
| | | 93 | | { |
| | 2 | 94 | | throw new InvalidOperationException( |
| | 2 | 95 | | $"{optionPath}.{nameof(SqsSubscriberOptions.VisibilityRenewalInterval)} requires " + |
| | 2 | 96 | | $"{nameof(SqsSubscriberOptions.VisibilityTimeout)} so the heartbeat knows how far to extend each mes |
| | | 97 | | } |
| | | 98 | | |
| | 2 | 99 | | if (renewalInterval >= renewedVisibility) |
| | | 100 | | { |
| | 2 | 101 | | throw new InvalidOperationException( |
| | 2 | 102 | | $"{optionPath}.{nameof(SqsSubscriberOptions.VisibilityRenewalInterval)} must be shorter than " + |
| | 2 | 103 | | $"{nameof(SqsSubscriberOptions.VisibilityTimeout)}, or messages become visible between heartbeats.") |
| | | 104 | | } |
| | | 105 | | } |
| | | 106 | | |
| | 3 | 107 | | switch (subscriberOptions.AckMode) |
| | | 108 | | { |
| | | 109 | | case SqsAckMode.AckAfterHandlerCompletes: |
| | 3 | 110 | | return; |
| | | 111 | | |
| | | 112 | | case SqsAckMode.AckAfterEnqueue: |
| | 3 | 113 | | if (subscriberOptions.BackgroundWorkerCount <= 0) |
| | | 114 | | { |
| | 2 | 115 | | throw new InvalidOperationException( |
| | 2 | 116 | | $"{optionPath}.{nameof(SqsSubscriberOptions.BackgroundWorkerCount)} must be explicitly configure |
| | 2 | 117 | | $"when {nameof(SqsSubscriberOptions.AckMode)} is {nameof(SqsAckMode.AckAfterEnqueue)}."); |
| | | 118 | | } |
| | | 119 | | |
| | 3 | 120 | | if (subscriberOptions.BackgroundQueueCapacity <= 0) |
| | | 121 | | { |
| | 2 | 122 | | throw new InvalidOperationException( |
| | 2 | 123 | | $"{optionPath}.{nameof(SqsSubscriberOptions.BackgroundQueueCapacity)} must be explicitly configu |
| | 2 | 124 | | $"when {nameof(SqsSubscriberOptions.AckMode)} is {nameof(SqsAckMode.AckAfterEnqueue)}."); |
| | | 125 | | } |
| | | 126 | | |
| | 3 | 127 | | if (subscriberOptions.BackgroundDrainTimeout <= TimeSpan.Zero) |
| | | 128 | | { |
| | 2 | 129 | | throw new InvalidOperationException( |
| | 2 | 130 | | $"{optionPath}.{nameof(SqsSubscriberOptions.BackgroundDrainTimeout)} must be positive."); |
| | | 131 | | } |
| | | 132 | | |
| | | 133 | | // SQS subscribers spend only the background drain at shutdown; the receive loop |
| | | 134 | | // stops with the host token and the SDK client needs no bounded close. |
| | 3 | 135 | | ShutdownBudgetValidator.Validate( |
| | 3 | 136 | | "SQS", |
| | 3 | 137 | | $"{nameof(SqsAsyncResponseOptions)}.{nameof(SqsAsyncResponseOptions.HostShutdownTimeout)}", |
| | 3 | 138 | | transportOptions.HostShutdownTimeout, |
| | 3 | 139 | | ($"{optionPath}.{nameof(SqsSubscriberOptions.BackgroundDrainTimeout)}", subscriberOptions.Background |
| | | 140 | | |
| | 3 | 141 | | return; |
| | | 142 | | |
| | | 143 | | default: |
| | 2 | 144 | | throw new InvalidOperationException( |
| | 2 | 145 | | $"{optionPath}.{nameof(SqsSubscriberOptions.AckMode)} has unsupported value '{subscriberOptions.AckM |
| | | 146 | | } |
| | | 147 | | } |
| | | 148 | | |
| | | 149 | | public static string Required(string? value, string name) |
| | 3 | 150 | | => !string.IsNullOrWhiteSpace(value) |
| | 3 | 151 | | ? value |
| | 3 | 152 | | : throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{name} must be configured."); |
| | | 153 | | |
| | | 154 | | private static void Positive(TimeSpan value, string name) |
| | | 155 | | { |
| | 3 | 156 | | if (value <= TimeSpan.Zero) |
| | 2 | 157 | | throw new InvalidOperationException($"{nameof(SqsAsyncResponseOptions)}.{name} must be positive."); |
| | 3 | 158 | | } |
| | | 159 | | } |