From c034b58c3b15617a5acc1930b637a15ff2eec108 Mon Sep 17 00:00:00 2001 From: Kevin C Date: Mon, 29 Jun 2026 09:18:27 -0500 Subject: [PATCH] Reuse existing SQS queue on install instead of failing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installer drives CloudPrint.Service.exe's `create-queue` subcommand to provision queues. CreateQueue passed Tags inline, so re-running install against a queue that already existed with different tags was rejected by SQS ("a queue already exists with the same name and different tags"). The attribute-mismatch fallback also filtered on the obsolete error code "QueueAlreadyExists", which AWSSDK.SQS v4 no longer emits — it throws QueueNameExistsException — so that catch never fired either. Apply tags via a separate additive TagQueue call rather than passing them to CreateQueue (relying only on SQS's documented attribute-based create idempotency), and catch QueueNameExistsException to reconcile attributes on a pre-existing queue. Applies to both the main queue and its DLQ. --- src/CloudPrint.Service/Program.cs | 89 ++++++++++++++++++------------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/src/CloudPrint.Service/Program.cs b/src/CloudPrint.Service/Program.cs index 4687be1..90dcf2b 100644 --- a/src/CloudPrint.Service/Program.cs +++ b/src/CloudPrint.Service/Program.cs @@ -616,64 +616,81 @@ static async Task CreateQueue(string queueName, Dictionary? using var sqsClient = new AmazonSQSClient( accessKey, secretKey, RegionEndpoint.GetBySystemName(region)); - // Create DLQ first + var queueTags = tags ?? new Dictionary(); + + // Tags are applied separately (via TagQueue below) rather than passed into + // CreateQueue. SQS rejects CreateQueue when a queue of the same name already + // exists with different tags ("a queue already exists with the same name and + // different tags"), so provisioning must not depend on tags matching at create + // time. TagQueue is additive and never fails on an existing queue. + + // Create the DLQ first, reusing it if it already exists. var dlqName = $"{queueName}-dlq"; - var dlqResponse = await sqsClient.CreateQueueAsync(new CreateQueueRequest - { - QueueName = dlqName, - Tags = tags ?? new Dictionary() - }); + var dlqUrl = await EnsureQueue(dlqName, attributes: null); // Get DLQ ARN var dlqAttributes = await sqsClient.GetQueueAttributesAsync(new GetQueueAttributesRequest { - QueueUrl = dlqResponse.QueueUrl, + QueueUrl = dlqUrl, AttributeNames = ["QueueArn"] }); var dlqArn = dlqAttributes.Attributes["QueueArn"]; - // Create or update main queue with redrive policy - string queueUrl; - try + // Create the main queue with its redrive policy, reusing it if it already exists. + var queueUrl = await EnsureQueue(queueName, new Dictionary + { + ["RedrivePolicy"] = JsonSerializer.Serialize(new { deadLetterTargetArn = dlqArn, maxReceiveCount = 5 }) + }); + + // Apply tags additively to both queues — safe whether they were just created or + // already existed (TagQueue overwrites matching keys and adds new ones). + await TagIfAny(dlqUrl); + await TagIfAny(queueUrl); + + Console.WriteLine(queueUrl); + return 0; + + // Creates the named queue with the given attributes, or reuses an existing queue + // and brings its attributes in line. Tags are handled separately by TagIfAny so a + // tag mismatch on an existing queue can't fail provisioning. + async Task EnsureQueue(string name, Dictionary? attributes) { - var response = await sqsClient.CreateQueueAsync(new CreateQueueRequest + try { - QueueName = queueName, - Attributes = new Dictionary + var response = await sqsClient.CreateQueueAsync(new CreateQueueRequest { - ["RedrivePolicy"] = JsonSerializer.Serialize(new { deadLetterTargetArn = dlqArn, maxReceiveCount = 5 }) - }, - Tags = tags ?? new Dictionary() - }); - queueUrl = response.QueueUrl; - } - catch (AmazonSQSException ex) when (ex.ErrorCode == "QueueAlreadyExists") - { - var urlResponse = await sqsClient.GetQueueUrlAsync(queueName); - queueUrl = urlResponse.QueueUrl; - - await sqsClient.SetQueueAttributesAsync(new SetQueueAttributesRequest + QueueName = name, + Attributes = attributes ?? new Dictionary() + }); + return response.QueueUrl; + } + catch (QueueNameExistsException) { - QueueUrl = queueUrl, - Attributes = new Dictionary + // Same name, different attributes — reuse the queue and update its attributes. + var url = (await sqsClient.GetQueueUrlAsync(name)).QueueUrl; + if (attributes is { Count: > 0 }) { - ["RedrivePolicy"] = JsonSerializer.Serialize(new { deadLetterTargetArn = dlqArn, maxReceiveCount = 5 }) + await sqsClient.SetQueueAttributesAsync(new SetQueueAttributesRequest + { + QueueUrl = url, + Attributes = attributes + }); } - }); + return url; + } + } - // Apply tags to the existing queue (additive — doesn't remove other tags) - if (tags is { Count: > 0 }) + async Task TagIfAny(string url) + { + if (queueTags.Count > 0) { await sqsClient.TagQueueAsync(new TagQueueRequest { - QueueUrl = queueUrl, - Tags = tags + QueueUrl = url, + Tags = queueTags }); } } - - Console.WriteLine(queueUrl); - return 0; } catch (Exception ex) {