-
Notifications
You must be signed in to change notification settings - Fork 29.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AbortSignal.any() not destroyed when goes out of scope #48419
Comments
Example: #!/usr/bin/env node
import { setFlagsFromString } from "v8";
import { runInNewContext } from "vm";
setFlagsFromString( "--expose_gc" );
const gc = runInNewContext( "gc" );
const ac = new AbortController();
{
let s = AbortSignal.any( [ac.signal] );
s.addEventListener( "abort", console.log );
s = null;
}
gc();
ac.abort(); Listener is called, even if signal is destroyed and V8 garbage collector is executed. |
Another example: #!/usr/bin/env node
import watch from "#core/devel/watch";
import gc from "#core/devel/garbage-collector";
{
let s = {}
watch( s ).on( "destroy", () => console.log( "--- DESTROY 1" ) );
s = null;
gc();
}
{
let s = AbortSignal.any( [] );
watch( s ).on( "destroy", () => console.log( "--- DESTROY 2" ) );
s = null;
gc();
} Output: --- DESTROY 1
As you can see, signals, created with |
Let me know if I misunderstand you but |
As I see in the node sources - this method is nodejs specific. Also, it now contain memory leak. I tried to run it un the loop anf see, that resident memory growth without lconstantly. |
Please, reopen this issue, it requires fix. |
this behavior is expected and is more related to events than to in your example s.addEventListener( "abort", console.log ); creates an event listener, but nothing ever removes it, hence the memory leak. const handler = (...args) => {
s.removeEventListener("abort", handler);
console.log(...args);
}
s.addEventListener( "abort", handler); or simply s.addEventListener( "abort", console.log, { once: true }); |
It contains mem leak. Also, just for info, it works very slow. for ( let n = 0; n < 3; n++ ) {
for ( let n = 0; n < 500_000; n++ ) {
const a = AbortSignal.any( [] );
}
console.log( process.memoryUsage.rss() );
} output: 1300037632
2685407232
3345960960 For example, for ( let n = 0; n < 3; n++ ) {
for ( let n = 0; n < 500_000; n++ ) {
const a = new AbortController();
}
console.log( process.memoryUsage.rss() );
} output: 37724160
38465536
38465536 |
I am reopening. |
CC @atlowChemi |
The behavior is because of how Weakrefs work, internally AbortSignal.any uses weakrefs and they will not get GCed until after a turn of the event loop. e.g.:
The any function always allocates a weakref (even when receiving an empty array). For the empty array case, I think that a guard can/should be added so that it won't allocate the relatively heavy object, but it won't solve the general "issue".
|
Pkease, add method, whist will allow correctly destory it, unlink from all watched signals and remove all listeners. Alos, it will be handy to add / reomve signals after it was constructed. |
As already stated, AbortSignal is a web platform API (yes it is implemented by Node itself, but it complies to a standard). |
Hey, mem leak is still present in node 20.4.0, Try to run this code in a loop, you will see, that memory rss grows without a limit.
|
I don;t know the particular details of the implementation, but if you are using WeakRefs you need to watch for garbagee collection using FinalizationRegistry anf remove refs manually from the set, otherwise they wull never be deleted. Please, re-open this issue. Also , it works very slow, for example, speed is comparable with execution of the sql query to the remove sql server. |
Hi @zdm not sure exactly how what you executed, but as @Linkgoron mentioned:
In addition, an |
Could you, please run this:
You will see that memory not freeing, |
Today node 20.4 was released, it includes your patch, which was directed to fix mem leak. |
Is it possible to remove this mem leak or not? |
As stated - you will never see the memory freeing without freeing the thread-pool. Using FinalizationRegistry will not help you, as internally they also use weak references, and in addition there is no guarantee on when the callback will be called. As @atlowChemi noted, it's unclear that the WeakRef is actually the issue here regarding performance (see the nodejs/performance issues). In addition, there is no guarantee from the GC side on which objects will actually be collected and when. I would suggest heavily that your code should not be dependent on GC behaviour. You are welcome to provide PRs that improve the performance and keep the correctness of the |
Yes. you are right. |
PR-URL: nodejs#48448 Fixes: nodejs#48419 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Nitzan Uziely <linkgoron@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
PR-URL: nodejs#48448 Fixes: nodejs#48419 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Nitzan Uziely <linkgoron@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
PR-URL: nodejs#48448 Fixes: nodejs#48419 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Nitzan Uziely <linkgoron@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
I'd like to revisit this to better understand and point what I think is a leak still, even when ticks are processing. Case 1: Tight loop (will OOM) - discussed aboveBased on my understanding, running in a tight loop will cause an OOM. I don't like it, but I see the reasoning above: while ( 1 ) {
AbortSignal.any( [new AbortController().signal, new AbortController().signal] );
} Case 2: Allow ticks to occur (will not OOM) - implied to be OK aboveAllowing ticks will do GC, and we won't OOM: let i = 0;
setInterval(() => {
i++;
AbortSignal.any( [new AbortController().signal, new AbortController().signal] );
if(i % 10000 === 0) {
console.log(i, new Date());
}
}, 0); In this case, the inspector shows a slow climb in Case 3: Allow ticks to occur, lose the derived signal, but keep one original signals - a leak?In a variation of case 2, which I feel fits the real world, I generate Signal.any with one new signal and one long-lived signal. let i = 0;
let ac = new AbortController();
setInterval(() => {
i++;
AbortSignal.any( [ac.signal, new AbortController().signal] );
if(i % 10000 === 0) {
console.log(i, new Date());
}
}, 0); In case 3, I see a steady climb in My use of setInterval probably is artificially slowing down how fast I can run this test. Perhaps there's a faster way to prove/disprove my observation that this is a permanent memory leak? I'm curious if I've misunderstood prior comments, or if we should just expect that use of Thank you. |
Version
20.0.3
Platform
all
Subsystem
all
What steps will reproduce the bug?
AbortSignal.any( ...signals )
add events listeners to all passed signals and returns newAbortSignal
instance.This instance is not destroyed, when goes out of scope, because has listeners set.
And there is no possibility to unsubscribe from the child signals.
So, pltase, add method, which will allow to perform cleanup, for example
clear()
.How often does it reproduce? Is there a required condition?
Please, see above.
What is the expected behavior? Why is that the expected behavior?
Please, see above.
What do you see instead?
Please, see above.
Additional information
Please, see above.
The text was updated successfully, but these errors were encountered: