fix(toolkit-lib): clean up the loser of noOlderThan()'s timer/resolver race - #1870
Open
Adityaj0 wants to merge 1 commit into
Open
fix(toolkit-lib): clean up the loser of noOlderThan()'s timer/resolver race#1870Adityaj0 wants to merge 1 commit into
Adityaj0 wants to merge 1 commit into
Conversation
…r race fixes aws#1869 BackgroundStackRefresh.noOlderThan() raced a "wait for refresh" promise against a "reject after ms" setTimeout via Promise.race(), without cleaning up whichever side lost: - When the refresh landed first (the common case under cdk gc's polling loop), the losing setTimeout handle was never captured or cleared, keeping the event loop alive and its reject closure retained until it eventually fired on its own. - When the timeout won instead, the resolve callback already pushed into queuedPromises for the losing branch was never removed. Only justRefreshedStacks() drains that array, so the stale resolver sat there forever -- under many concurrent timed-out noOlderThan() calls, queuedPromises grows without bound. Restructure noOlderThan() around a single Promise executor that clears the timeout on a successful refresh, and removes its own entry from queuedPromises on a timeout, so the loser is always cleaned up regardless of which side wins. Added three regression tests: the timeout handle is cleared once a refresh lands, a single timed-out call leaves no dangling entry in queuedPromises, and 25 concurrent timed-out calls don't accumulate entries either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adityaj0
requested a deployment
to
integ-approval
August 21, 2026 02:33 — with
GitHub Actions
Waiting
aws-cdk-automation
enabled auto-merge
August 21, 2026 02:33
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1870 +/- ##
=======================================
Coverage 91.10% 91.10%
=======================================
Files 80 80
Lines 12205 12205
Branches 1742 1742
=======================================
Hits 11119 11119
Misses 1050 1050
Partials 36 36
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fixes #1869
Reason for this change
BackgroundStackRefresh.noOlderThan()races a "wait for the background refresh" promise against a "reject afterms"setTimeoutviaPromise.race(), but never cleans up whichever side loses:setTimeouthandle is never captured, so it's never cleared -- it keeps the event loop alive and itsrejectclosure retained until it eventually fires on its own.resolvecallback already pushed intoqueuedPromisesfor the losing branch is never removed. OnlyjustRefreshedStacks()ever drains that array, so the stale resolver sits there forever -- undercdk gc's polling loop with many concurrent timed-outnoOlderThan()calls,queuedPromisesgrows without bound.Description of changes
Restructured
noOlderThan()around a singlePromiseexecutor instead ofPromise.race()over two independently-constructed promises:onRefreshclears the timeout handle before resolving.onRefreshentry fromqueuedPromises(viaindexOf/splice) before rejecting.Whichever side wins, the other side's state is always cleaned up.
Testing
Added three tests to
test/api/garbage-collection/garbage-collection.test.ts's existingBackgroundStackRefreshdescribe block:noOlderThan()clears its timeout handle once the refresh lands (asserts viajest.getTimerCount(), following the pattern already used elsewhere in this file for thestop()test).queuedPromises.queuedPromiseseither.All existing
BackgroundStackRefreshtests (6) and the new ones (3) pass; fullgarbage-collection.test.tssuite (32 tests) passes.Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license