| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Fix unbounded memory growth in AbortSignal.any() when called in tight loops without abort listeners. Root cause: Signals were being added to gcPersistentSignals immediately in AbortSignal.any(), preventing garbage collection even when no abort listeners existed. The gcPersistentSignals set would grow unbounded, causing OOM crashes in production (36 MB → 1.3 GB reported). Fix: Remove premature addition of signals to gcPersistentSignals. Signals are now only added when abort listeners are actually registered (via kNewListener), which was the intended behavior. Memory impact: - Before: ~1.5 MB growth per 1,000 iterations → OOM at ~300k iterations - After: Stable memory with normal GC variance No breaking changes. Behavior with abort listeners remains identical. Signals without listeners can now be properly garbage collected. Fixes: #54614
|
Update: This PR has been rebased to contain ONLY the AbortSignal.any() memory leak fix. The Promise.race() fix has been separated into #60184 for independent review. cc @nodejs/async_hooks @nodejs/web-standards Ready for review - please allow 48h for community feedback per Node.js contribution guidelines. |
Sorry, something went wrong.
|
It seems you are using a LLM, please stop, this is not bringing any value and is wasting our time. If you are not using one, please read and follow our contributing guidelines. |
Sorry, something went wrong.
My bad, will work on its guadrails and will not test it on node prs anymore. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Description
This PR fixes a critical memory leak in AbortSignal.any() that causes unbounded memory growth in production environments, growing from 36 MB to over 1.3 GB and leading to OOM crashes.
Fixes: #54614
Root Cause
When AbortSignal.any() is called in tight loops without attaching abort listeners, signals are prematurely added to the gcPersistentSignals SafeSet to prevent garbage collection. However, without listeners, these signals are NEVER removed, causing the set to grow unbounded.
The code was adding signals to gcPersistentSignals in two places:
This prevented V8 from garbage collecting signals even when they had no listeners and were no longer referenced.
The Fix
Removed premature addition of signals to gcPersistentSignals. Signals are now only added when abort listeners are actually registered, which is already correctly handled by the kNewListener method (lines 312-326).
Before:
After:
The kNewListener method already handles GC prevention correctly when listeners are attached:
Evidence
Memory Leak Confirmed (Node.js v22.18.0)
Running reproduction with --max-old-space-size=512:
Growth Rate: ~1.5 MB per 1,000 iterations
Result: OOM crash at 300k iterations with 512 MB heap
Test Case
Added test/parallel/test-abortcontroller-any-memory-leak.js which tests:
All tests assert memory growth stays under reasonable thresholds (50-100 MB for 50k-100k iterations).
Without fix: Memory grows hundreds to thousands of MBs
With fix: Stable memory within GC variance
Performance Impact
Before:
After:
Backward Compatibility
No breaking changes. This fix only changes WHEN signals are added to gcPersistentSignals:
Behavior with abort listeners remains identical. Signals are still prevented from GC when needed.
Checklist