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

Information
Class: AsyncResponse.Transports.SQS.SqsQueueProvisioningService
Assembly: AsyncResponse.Transports.SQS
File(s): /_/src/Transports/AsyncResponse.Transports.SQS/SqsQueueProvisioningService.cs
Line coverage
98%
Covered lines: 59
Uncovered lines: 1
Coverable lines: 60
Total lines: 140
Line coverage: 98.3%
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%
StartAsync()100%66100%
StopAsync(...)100%11100%
EnsureQueueWithDeadLetterAsync()100%22100%
CreateOrUpdateQueueAsync()100%11100%
CreateAsync()100%6690.9%
RetryAsync()100%11100%

File(s)

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

#LineLine coverage
 1using Amazon.SQS;
 2using Amazon.SQS.Model;
 3using Microsoft.Extensions.Hosting;
 4using Microsoft.Extensions.Logging;
 5using Microsoft.Extensions.Options;
 6using System.Text.Json;
 7
 8namespace AsyncResponse.Transports.SQS;
 9
 10/// <summary>
 11/// Provisions the worker and response queues (each with a dead-letter queue wired through a native
 12/// redrive policy) when <see cref="SqsAsyncResponseOptions.CreateQueues"/> is enabled. Registered
 13/// before the subscribers so provisioning completes before consumption starts; queues configured as
 14/// URLs are assumed to exist and are skipped.
 15/// </summary>
 21416internal sealed class SqsQueueProvisioningService(
 21417    IOptions<SqsAsyncResponseOptions> options,
 21418    ISqsClient client,
 21419    ILogger<SqsQueueProvisioningService> logger) : IHostedService
 20{
 21    private const int MaxAttempts = 40;
 22
 23    /// <summary>Creates the configured queues and their dead-letter pairs.</summary>
 24    public async Task StartAsync(CancellationToken cancellationToken)
 25    {
 21026        var o = options.Value;
 21027        if (!o.CreateQueues)
 228            return;
 29
 20830        SqsOptionsValidator.ValidateCommon(o);
 31
 124832        foreach (var queue in new[] { o.WorkerQueue, o.ResponseQueue })
 33        {
 41634            if (SqsQueueAddress.IsUrl(queue))
 35            {
 236                logger.LogInformation("SQS queue {Queue} is configured as a URL; skipping provisioning.", queue);
 237                continue;
 38            }
 39
 41440            await EnsureQueueWithDeadLetterAsync(o, queue, cancellationToken).ConfigureAwait(false);
 41        }
 21042    }
 43
 44    /// <summary>No-op.</summary>
 19645    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
 46
 47    internal async Task EnsureQueueWithDeadLetterAsync(
 48        SqsAsyncResponseOptions o,
 49        string queueName,
 50        CancellationToken cancellationToken)
 51    {
 41652        var fifo = SqsQueueAddress.IsFifo(queueName);
 53        // A FIFO queue's dead-letter queue must itself be FIFO, and FIFO names must end in ".fifo".
 41654        var deadLetterQueueName = SqsQueueAddress.DeriveDeadLetterQueueName(queueName, o.DeadLetterQueueSuffix);
 55
 41656        var fifoAttributes = fifo
 41657            ? new Dictionary<string, string>(StringComparer.Ordinal) { [QueueAttributeName.FifoQueue] = "true" }
 41658            : new Dictionary<string, string>(StringComparer.Ordinal);
 59
 41660        var deadLetterQueueUrl = await CreateOrUpdateQueueAsync(deadLetterQueueName, fifoAttributes, cancellationToken)
 41661            .ConfigureAwait(false);
 41662        var deadLetterQueueArn = await RetryAsync(
 41663            () => client.GetQueueArnAsync(deadLetterQueueUrl, cancellationToken),
 41664            cancellationToken).ConfigureAwait(false);
 65
 41666        var attributes = new Dictionary<string, string>(fifoAttributes, StringComparer.Ordinal)
 41667        {
 41668            [QueueAttributeName.RedrivePolicy] = AsyncResponseJson.Serialize(new Dictionary<string, string>
 41669            {
 41670                ["deadLetterTargetArn"] = deadLetterQueueArn,
 41671                ["maxReceiveCount"] = o.MaxReceiveCount.ToString()
 41672            })
 41673        };
 74
 41675        await CreateOrUpdateQueueAsync(queueName, attributes, cancellationToken).ConfigureAwait(false);
 76
 41677        logger.LogInformation(
 41678            "SQS queue {Queue} provisioned with dead-letter queue {DeadLetterQueue} (maxReceiveCount={MaxReceiveCount}).
 41679            queueName,
 41680            deadLetterQueueName,
 41681            o.MaxReceiveCount);
 41682    }
 83
 84    private async Task<string> CreateOrUpdateQueueAsync(
 85        string queueName,
 86        IReadOnlyDictionary<string, string> attributes,
 87        CancellationToken cancellationToken)
 88    {
 83289        return await RetryAsync(CreateAsync, cancellationToken).ConfigureAwait(false);
 90
 91        async Task<string> CreateAsync()
 92        {
 93            try
 94            {
 83695                return await client.CreateQueueAsync(queueName, attributes, cancellationToken).ConfigureAwait(false);
 96            }
 97            catch (QueueNameExistsException)
 98            {
 99                // The queue exists with different attributes (for example a redrive policy set by an
 100                // earlier run against a different DLQ ARN): converge by re-applying the attributes.
 101                // FifoQueue is create-only — SetQueueAttributes rejects it with InvalidAttributeName,
 102                // and RetryAsync then burned every attempt on that deterministic error before
 103                // aborting host startup (the FIFO dead-letter queue first, since it is created
 104                // with that attribute alone) — so it is dropped from the update; the queue is
 105                // already FIFO by the fact of its name.
 14106                var queueUrl = await client.GetQueueUrlAsync(queueName, cancellationToken).ConfigureAwait(false);
 14107                var updatable = new Dictionary<string, string>(StringComparer.Ordinal);
 60108                foreach (var attribute in attributes)
 109                {
 16110                    if (!string.Equals(attribute.Key, QueueAttributeName.FifoQueue.Value, StringComparison.Ordinal))
 8111                        updatable[attribute.Key] = attribute.Value;
 112                }
 113
 14114                if (updatable.Count > 0)
 8115                    await client.SetQueueAttributesAsync(queueUrl, updatable, cancellationToken).ConfigureAwait(false);
 14116                return queueUrl;
 117            }
 0118        }
 1664119    }
 120
 121    private async Task<T> RetryAsync<T>(Func<Task<T>> operation, CancellationToken cancellationToken)
 122    {
 1248123        var o = options.Value;
 1252124        for (var attempt = 1; ; attempt++)
 125        {
 126            try
 127            {
 1252128                return await operation().ConfigureAwait(false);
 129            }
 4130            catch (Exception ex) when (attempt < MaxAttempts && !cancellationToken.IsCancellationRequested)
 131            {
 132                // Startup ordering against a fresh endpoint (LocalStack still booting, transient
 133                // networking) resolves within a few retries; anything persistent still surfaces.
 4134                var delay = AsyncResponseRetry.Backoff(attempt, o.SubscriberRetryBaseDelay, o.SubscriberRetryMaxDelay);
 4135                logger.LogWarning(ex, "SQS queue provisioning attempt {Attempt} failed; retrying in {Delay}.", attempt, 
 4136                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
 137            }
 138        }
 1248139    }
 140}