Skip to content

lib: fix AbortSignal.any() observed-composite leak - #64481

Open
bitpshr wants to merge 1 commit into
nodejs:mainfrom
bitpshr:lib/abortsignal-any-observed-leak
Open

lib: fix AbortSignal.any() observed-composite leak#64481
bitpshr wants to merge 1 commit into
nodejs:mainfrom
bitpshr:lib/abortsignal-any-observed-leak

Conversation

@bitpshr

@bitpshr bitpshr commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Fixes a memory leak in AbortSignal.any() where an observed composite that follows a long-lived source keeps accumulating in that source's kDependantSignals, even across a forced global.gc().

The retainer is gcPersistentSignals. When you add an abort listener, the observed composite is added there so it stays alive long enough to fire. When one of its sources aborts, abortSignal() marks the composite aborted and runs its abort steps, but it never removes the composite from gcPersistentSignals. That set is only pruned when the listener is explicitly removed or a source is GC'd, so with a long-lived source the aborted composite is retained forever, never collected, and its WeakRef is never pruned from the source's dependant set.

An aborted composite can never fire again, so this drops each transitively-aborted dependent from gcPersistentSignals in the abort path, mirroring the removal that already happens for the directly-aborted signal a few lines down. Once it's collectable, the existing FinalizationRegistry prunes its entry from every source it followed.

The added test observes a long-lived source across many aborted composites and asserts the dependant set drains to zero. It fails before this change (the set stays at the iteration count) and passes after.

Fixes: #64476

@bitpshr

bitpshr commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Hi @geeksilva97 @atlowChemi, wanted to keep this on your radar since you reviewed #62367. This is a direct follow-up: transitively-aborted observed composites stay pinned in gcPersistentSignals, so they never get pruned from a long-lived source's kDependantSignals. Small fix plus a regression test. Whenever you get a chance.

@atlowChemi atlowChemi added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 23, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jul 23, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.14%. Comparing base (4a5eb1c) to head (157c19a).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64481      +/-   ##
==========================================
- Coverage   90.15%   90.14%   -0.01%     
==========================================
  Files         744      744              
  Lines      242517   242524       +7     
  Branches    45688    45688              
==========================================
- Hits       218642   218635       -7     
- Misses      15358    15365       +7     
- Partials     8517     8524       +7     
Files with missing lines Coverage Δ
lib/internal/abort_controller.js 95.34% <100.00%> (+0.05%) ⬆️

... and 25 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@geeksilva97

Copy link
Copy Markdown
Contributor

Thanks, @bitpshr

@geeksilva97 geeksilva97 added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Jul 23, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@atlowChemi

Copy link
Copy Markdown
Member

@bitpshr could you try to rebase? I think the Jenkins failure would be fixed by a rebase

An observed composite signal (one with an `abort` listener) is added to
gcPersistentSignals so it stays alive long enough to fire. When one of
its sources aborted, the composite was marked aborted but never removed
from that set, so it was retained forever and its WeakRef was never
pruned from a long-lived source's kDependantSignals. That set therefore
grew without bound.

Drop each transitively-aborted dependent from gcPersistentSignals, the
same way the directly-aborted signal is already removed, so it can be
collected and its dependant entries pruned.

Fixes: nodejs#64476
Signed-off-by: Paul Bouchon <mail@bitpshr.net>
@bitpshr
bitpshr force-pushed the lib/abortsignal-any-observed-leak branch from 1204b62 to 157c19a Compare July 26, 2026 17:27
@bitpshr

bitpshr commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main, thanks @atlowChemi.

@bitpshr

bitpshr commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased, but the two red checks are unrelated flakes rather than anything from this change. test-macOS failed in the "unusual chars folder" re-run step (a debugger test), and the aarch64 shared-boringssl build hit ERR_INSPECTOR_COMMAND: Promise was collected in test-repl-context (the log shows failed 3 out of 10, so it's flaky). This PR only touches AbortSignal.any() cleanup, so a re-run should clear both.

@geeksilva97 geeksilva97 added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 27, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jul 27, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs that have at least one approval, no pending requests for changes, and a CI started. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AbortSignal.any(): observed composites still accumulate in a long-lived source's kDependantSignals after #62367 (not GC-pruned)

4 participants