Skip to content

BackgroundStackRefresh.noOlderThan() leaks a timer (and a queued resolver on timeout) #1869

Description

@Adityaj0

Describe the bug

BackgroundStackRefresh.noOlderThan() in packages/@aws-cdk/toolkit-lib/lib/api/garbage-collection/stack-refresh.ts races two promises without cleaning up the loser:

return Promise.race([
  new Promise(resolve => this.queuedPromises.push(resolve)),
  new Promise((_, reject) => setTimeout(() => reject(new ToolkitError(...)), ms)),
]);

Two problems:

  1. When the "wait for the background refresh" side wins (the common case -- cdk gc calls noOlderThan() repeatedly while polling), the setTimeout handle for the losing "reject after ms" branch is never captured, so it's never cleared. It keeps the event loop alive and its reject closure retained until it eventually fires on its own.
  2. When the "reject after ms" side wins instead (a slow/stuck background refresh), the resolve callback that was pushed into this.queuedPromises for the losing branch is never removed. justRefreshedStacks() is the only thing that ever drains queuedPromises, so that stale resolver sits there forever. Under cdk gc's polling loop with many noOlderThan() calls timing out, this array grows without bound.

Expected Behavior

Whichever side of the race wins, the loser's timer should be cleared and, on a timeout, the loser's queued resolver should be removed from queuedPromises.

Observed Behavior

Neither cleanup happens today -- confirmed by reading the code and by the existing test suite's timer-count assertions used elsewhere in the same test file (jest.getTimerCount() after stop()), which show the pattern the codebase already expects timers to follow.

What's the environment?

  • aws-cdk-cli main branch
  • N/A (reproducible via unit test asserting jest.getTimerCount() / queuedPromises.length across repeated noOlderThan() calls, not environment-specific)

Other

I'll follow up with a PR that restructures noOlderThan() to clear the timeout on a successful refresh and to remove the queued resolver on a timeout, plus regression tests for both.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions