fix(aws): stop generated gateway tests leaking SNS topics and SQS queues - #4298
fix(aws): stop generated gateway tests leaking SNS topics and SQS queues#4298iancooper wants to merge 9 commits into
Conversation
The generated MessageGateway tests never deleted the infrastructure they created. Their providers' CleanUp/CleanUpAsync purged and disposed the channel and producer but left the topic and queue behind, on every run, pass or fail. That leaked ~26k resources across two regions and pushed eu-west-1 to exactly 1,000 FIFO SNS topics -- the AWS per-region quota -- so CI could no longer create topics. The cleanup script did not catch them either: - Its tag query filters on Environment=Test, but the gateway only stamps Source=Brighter, so the query returned nothing. - Its naming fallback listed the hand-written fixtures' prefixes, which predate the test generator. None of the 26k leaked names matched. - Its SQS listing had no --page-size, so it saw at most 1000 queues and the rest were silently invisible. - Deleting one resource per AWS CLI call could not clear a real backlog inside the workflow's 5 minute timeout. The test for the script only ever created resources tagged Environment=Test, so it passed while the script found none of the real leaks. Providers now track every topic and queue name they hand out, including dead letter queues, and delete them from CleanUp via AwsTestResourceReaper. Deleting by name rather than by created object also covers a test that failed while standing its infrastructure up, when there is no producer or channel left to dispose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq
The sweep deletes in parallel now, but a backlog that has been allowed to build up runs to tens of thousands of resources and will not clear inside five minutes. Raise the timeout to 30. The diagnostic queue count called list-queues without --page-size, so it silently stopped at 1000 and made the backlog look an order of magnitude smaller than it was -- which is part of why this went unnoticed. Split from the preceding commit because pushing changes under .github/workflows/ needs a token with the workflow scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq
|
Review — fix(aws): stop generated gateway tests leaking SNS topics and SQS queues Thanks for the write-up; the diagnosis is unusually clear and the root cause is convincingly established. Reaping names rather than created objects is the right call — it survives a fixture that dies mid-setup and it picks up the DLQs and auto-created topics, which is exactly the class of leak that got us to 26,381 resources. The parallel sweep passes ARNs to A few things I would want addressed before merge, then some smaller notes. 1.
public async Task CleanUpAsync(IAmAMessageProducerAsync? producer, IAmAChannelAsync? channel, IEnumerable<Message> messages)
{
if (channel != null)
{
await channel.PurgeAsync(); // throws => reap never runs
channel.Dispose();
}
if (producer != null)
{
await producer.DisposeAsync(); // same
}
await _reaper.ReapAsync();
}The stated requirement is "cleans up even on a failed run", and the manual verification covers a test that fails in the test body. But a throw in Suggested shape: try
{
if (channel != null) { await channel.PurgeAsync(); channel.Dispose(); }
if (producer != null) await producer.DisposeAsync();
}
finally
{
await _reaper.ReapAsync();
}Same for the sync 2.
Related: 3. The broadened name sweep can now delete resources belonging to an in-flight CI run
The hazard technically pre-existed for the older prefixes, but this change makes it likely rather than unlikely. Worth considering an age guard — e.g. skip queues whose Smaller points
STS failure is not cached — same method, line 155: on failure The SQS providers track a channel name that is never created. The diagnostic count bug is only half fixed ( The PR description is out of date on the workflow commit. The body says the Test coverage The shell-side coverage is genuinely improved. Test 8 exercising untagged convention-named resources, with Test 5 as the negative counterpart, is the right pair, and generating a fresh GUID per run so the pattern is tested rather than a literal name is a nice touch. Two gaps:
Net: the design is right and the analysis behind it is excellent. Point 1 is the one I would insist on — eight small edits, and it is the difference between "leaks are fixed" and "leaks are fixed unless teardown itself has a bad day". Points 2 and 3 are close behind; the rest is polish. |
Review: fix(aws): stop generated gateway tests leaking SNS topics and SQS queuesThe root-cause work here is excellent — the four independent reasons the cleanup script never caught up are each verified rather than assumed, and the "track names, not objects" decision is right for exactly the reason given (a test that dies during setup has no producer or channel to dispose, but has already created the topic). Reusing A few things I would want addressed before merge, then some smaller notes. 1.
|
|
(review continued) 4.
|
The reaper ran as the last statement of each provider's CleanUp, after the channel purge and the producer dispose. Both reach AWS and can fail — PurgeQueue is throttled to one call per queue a minute, which CI load makes reachable — and a throw there skipped the reap entirely, leaking exactly the topics and queues the reaper exists to delete. Purge and dispose now run in a try, with the reap in a finally. ReapAsync had the same shape internally: its per-resource deletes were guarded but creating the SNS and SQS clients was not, so a failure there propagated out of teardown and replaced the test's own result — the opposite of what its summary promised. Each sweep now guards its own client, and the tracked names are cleared in a finally so a half-completed run cannot leave the reaper holding names it will never retry. Adds PendingTopics and PendingQueues so that single-attempt contract is visible, and a test that reaps through a connection whose client configuration fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq
Stands infrastructure up through a provider and then looks for it in AWS after CleanUpAsync, so the reap is verified against the account rather than against the fixture's own bookkeeping. The second test tears down through a channel that throttles on purge — the way teardown actually fails — and asserts both that the failure still surfaces and that the topic and queue went anyway; reverting any of the finally blocks fails it. Deletion is eventually consistent, and SQS documents a queue as visible for up to sixty seconds after DeleteQueue, so the assertions poll to a deadline rather than reading once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq
The SQS providers generated an sqs-<type>-ch-<guid> name and registered it for reaping, but CreateSubscription immediately replaces it with the publication's queue — point-to-point needs the two to match — so no queue by that name has ever existed. Teardown was spending a GetQueueUrl and a caught QueueDoesNotExistException on it every time, in both SQS suites. Tracking it after the override would not help: the effective name is the routing key's queue, which GetOrCreateRoutingKey already tracks, so it would cost the same round trip on an already-deleted queue. The name is simply left untracked, with a remark to keep the reason with the code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq
The partition was being read out of the caller identity's own ARN by position, under a comment describing an IAM user ARN; assumed-role ARNs happen to put the partition in the same slot, but nothing guarantees that. RegionEndpoint carries PartitionName directly, so only the account id needs looking up. The ARN is now built with Amazon.Arn, the same way ValidateTopicByArnConvention builds it, so the two constructions stay in step. A failed identity lookup was also not remembered: every topic retried STS, failed, and fell back to FindTopicAsync — a failing call added to each of the quadratic scans the lookup exists to avoid. It is now asked once. FindTopicAsync keeps no cancellation token: the SDK offers no overload that takes one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq
grep -c prints 0 and exits 1 when nothing matches, so the || echo "0" fired as well and the queue count came out as two lines — "Total queues: 0 0". The fallback is only there to survive the step's set -e, so || true is what was wanted. The topic counts had the undercount the queue count was already fixed for: length(@) is applied per page under auto-pagination, so it prints a number per page rather than a total. They now count names, like the queue count does. Test 8 read once for a resource that had just been deleted. SNS will answer get-topic-attributes for a deleted topic briefly, and SQS documents a queue as visible for up to sixty seconds, so the assertion now polls to a deadline — still failing if the resource genuinely survives. The convention names take their hex id from uuidgen, which ships wherever the AWS CLI does, rather than from openssl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq
The name sweep now matches (sqs|sns)-(std|fifo)(-ch)?-<32 hex>, which is most of what the AWS suites create, and it also runs on a six-hourly schedule. That combination can delete queues out from under a job that is still running and report it as a confusing flaky failure — trading a resource leak for CI flake. Queues younger than CLEANUP_MIN_AGE_SECONDS are left for the next sweep. A queue whose age cannot be read is treated as old, since the likeliest reason is that it has already gone. The lookups run at the sweep's parallelism, and only against names that already matched. The guard is only wanted when something might be in flight: the workflow sets it for the scheduled run and clears it for the run triggered by CI completing, which would otherwise defer every leak from that CI run by six hours. The sweep test clears it too — what it tests is which resources are matched, not when the sweep declines to act. SNS reports no creation time, so topics cannot be guarded this way and are still exposed on the scheduled path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq
ReviewI read the diff against The core of this is right, and the PR body is the best root-cause writeup I've read on this repo. Reaping names rather than created objects is the correct call and the Findings below, most severe first. 1.
|
|
Thanks — this was a good catch list. All of it is addressed in 1. 2. 3. The scheduled sweep vs. an in-flight run. Age guard added: queues younger than Two things the suggestion needed that were not obvious from the diff:
Smaller points. Not taken as written:
Test coverage. Two new suites:
One gap left open. SNS reports no creation time, so topics cannot be age-guarded the way queues can and remain exposed to the scheduled sweep. Narrowing the scheduled path to the tag query for topics would close it, at the cost of making a leaked topic wait for the next CI completion. Recorded in the commit message and the PR description rather than fixed here. CI is the first run for the live tests and the updated sweep test — they cannot run locally without credentials. |
Description
CI could no longer create AWS topics or queues. The account had accumulated 26,381 orphaned resources across two regions, including exactly 1,000 FIFO SNS topics in
eu-west-1— the AWS default quota for FIFO topics per account per region. That quota is what CI was hitting, and it is why most of the leftovers were.fifo.The leak. The generated MessageGateway tests never deleted the infrastructure they created. All eight AWS providers'
CleanUp/CleanUpAsyncpurged and disposed the channel and producer, then stopped — noDeleteTopicAsync/DeleteQueueAsync. They also built aChannelFactoryinline and discarded it, so they held no handle to delete with. Every generated test leaked its topic and queue on every run, pass or fail. The hand-written fixtures and the scheduler tests already did this correctly and were not the problem.Why
clean_failed_tests_aws_assets.shnever caught up with it:Environment=Test, but the gateway only stampsSource=Brighter(AWSMessagingGateway.cs:386), so it returned nothing.list-queueswas called without--page-size. SQS returns at most 1000 queues and noNextToken, so 20,900 queues looked like 1,000.And
test_clean_failed_tests_aws_assets.shonly ever created resources taggedEnvironment=Test, so it passed while the script found none of the actual leaks.The fix. Providers now track every topic and queue name they hand out — including dead letter queues — and delete them from teardown via a new
AwsTestResourceReaper. Reaping names rather than created objects also covers a test that failed while standing its infrastructure up, when there is no producer or channel left to dispose, and catches resources created as a side effect of a name (a DLQ, or a topic auto-created by a producer).The teardown abstraction itself is not new —
CleanUpAsynconIAmAMessageGatewayProactorProvideris exactly that hook. AWS simply was not honouring it. GCP and Kafka already delete their infrastructure through the same hook.Related Issues
Type of Change
Checklist
Additional Notes
Verification
test_clean_failed_tests_aws_assets.sh: 24/24 pass, including 4 new cases covering untagged, convention-named, FIFO resources. The existing negative test still proves untagged resources that do not match the convention survive.The review-response commits have been verified as far as they can be locally: both projects build, and the offline reaper tests pass. The new live-account tests and the updated sweep test have not been run against AWS yet — CI is their first run.
This PR carries a workflow-scoped change
.github/workflows/aws-cleanup.ymlis in the diff: the cleanup workflow'stimeout-minutesgoes from 5 to 30, the diagnostic resource counts are fixed, and the sweep's age guard is wired to the trigger. Pushing this branch therefore needs a token with theworkflowscope.Review response
Addressing #4298 (comment):
finally. It was the last statement of each provider'sCleanUp, after the channel purge and producer dispose — both of which reach AWS and can fail.PurgeQueueis throttled to one call per queue a minute, so a throw there was reachable under CI load, and it skipped the reap entirely.ReapAsynccan no longer throw past teardown. Creating the SNS and SQS clients sat outside the guarded region, so a failure there replaced the test's own result — the opposite of what the method's summary promised. Each sweep guards its own client, and the tracked names are cleared in afinallyso a half-completed run cannot leave names it will never retry.CLEANUP_MIN_AGE_SECONDS(default an hour) are left for the next pass. The guard applies to the six-hourly run only; the run triggered by CI completing has nothing in flight and still sweeps everything, so no leak is deferred. SNS reports no creation time, so topics remain exposed on the scheduled path.RegionEndpoint.PartitionNameandAmazon.Arn, the wayValidateTopicByArnConventionbuilds them, instead of reading the partition out of the caller identity's ARN by position. A failed STS lookup is now remembered rather than retried per topic.CreateSubscriptionreplaces it with the publication's queue, whichGetOrCreateRoutingKeyalready tracks — so tracking it after the override would cost the same wasted round trip rather than removing it.grep -cprints0and exits 1, so the|| echo "0"fired too and the queue count came out asTotal queues: 0 0;|| trueis what was wanted. The topic counts had the same per-pagelength(@)undercount the queue count was already fixed for.uuidgenrather thanopenssl.Two items were not taken as written:
FindTopicAsynckeeps no cancellation token — the SDK offers no overload that takes one.CreateSubscriptionwould not have removed the round trip, for the reason above; the name is left untracked instead.New tests.
MessageGatewayProviderCleanUpTestsstands infrastructure up through a provider and then looks for it in AWS afterCleanUpAsync, so the reap is verified against the account rather than the fixture's bookkeeping; a second case tears down through a channel that throttles on purge and asserts both that the failure surfaces and that the topic and queue went anyway. Deletion is eventually consistent, so the assertions poll.AwsTestResourceReaperTeardownFailureTestscovers the swallow-and-drain contract offline, with no credentials needed.Footgun worth knowing
The script and workflow operate on the ambient AWS region. The workflow pins
eu-west-1, but local test runs leak into each developer's own default region — that is where a separateus-east-1pile came from. Documented in the script header rather than widening the workflow's scope.🤖 Generated with Claude Code
https://claude.ai/code/session_012BpmKpyGj1G6VZ2EibRZoq