Skip to content

Commit 68bcec6

Browse files
geeksilva97ruyadorno
authored andcommitted
lib: remove settled dependant signals when they are GCed
PR-URL: #55354 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent b9200c3 commit 68bcec6

File tree

2 files changed

+145
-9
lines changed

2 files changed

+145
-9
lines changed

lib/internal/abort_controller.js

+23-9
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ function lazyMessageChannel() {
8585
}
8686

8787
const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);
88+
const dependantSignalsCleanupRegistry = new SafeFinalizationRegistry((signalWeakRef) => {
89+
const signal = signalWeakRef.deref();
90+
if (signal === undefined) {
91+
return;
92+
}
93+
signal[kDependantSignals].forEach((ref) => {
94+
if (ref.deref() === undefined) {
95+
signal[kDependantSignals].delete(ref);
96+
}
97+
});
98+
});
8899
const gcPersistentSignals = new SafeSet();
89100

90101
const kAborted = Symbol('kAborted');
@@ -244,24 +255,27 @@ class AbortSignal extends EventTarget {
244255
}
245256
signal[kDependantSignals] ??= new SafeSet();
246257
if (!signal[kComposite]) {
247-
resultSignal[kSourceSignals].add(new SafeWeakRef(signal));
258+
const signalWeakRef = new SafeWeakRef(signal);
259+
resultSignal[kSourceSignals].add(signalWeakRef);
248260
signal[kDependantSignals].add(resultSignalWeakRef);
261+
dependantSignalsCleanupRegistry.register(resultSignal, signalWeakRef);
249262
} else if (!signal[kSourceSignals]) {
250263
continue;
251264
} else {
252-
for (const sourceSignal of signal[kSourceSignals]) {
253-
const sourceSignalRef = sourceSignal.deref();
254-
if (!sourceSignalRef) {
265+
for (const sourceSignalWeakRef of signal[kSourceSignals]) {
266+
const sourceSignal = sourceSignalWeakRef.deref();
267+
if (!sourceSignal) {
255268
continue;
256269
}
257-
assert(!sourceSignalRef.aborted);
258-
assert(!sourceSignalRef[kComposite]);
270+
assert(!sourceSignal.aborted);
271+
assert(!sourceSignal[kComposite]);
259272

260-
if (resultSignal[kSourceSignals].has(sourceSignal)) {
273+
if (resultSignal[kSourceSignals].has(sourceSignalWeakRef)) {
261274
continue;
262275
}
263-
resultSignal[kSourceSignals].add(sourceSignal);
264-
sourceSignalRef[kDependantSignals].add(resultSignalWeakRef);
276+
resultSignal[kSourceSignals].add(sourceSignalWeakRef);
277+
sourceSignal[kDependantSignals].add(resultSignalWeakRef);
278+
dependantSignalsCleanupRegistry.register(resultSignal, sourceSignalWeakRef);
265279
}
266280
}
267281
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Flags: --expose_gc
2+
//
3+
import '../common/index.mjs';
4+
import { describe, it } from 'node:test';
5+
6+
function makeSubsequentCalls(limit, done, holdReferences = false) {
7+
let dependantSymbol;
8+
let signalRef;
9+
const ac = new AbortController();
10+
const retainedSignals = [];
11+
const handler = () => { };
12+
13+
function run(iteration) {
14+
if (iteration > limit) {
15+
// This setImmediate is necessary to ensure that in the last iteration the remaining signal is GCed (if not
16+
// retained)
17+
setImmediate(() => {
18+
global.gc();
19+
done(ac.signal, dependantSymbol);
20+
});
21+
return;
22+
}
23+
24+
if (holdReferences) {
25+
retainedSignals.push(AbortSignal.any([ac.signal]));
26+
} else {
27+
// Using a WeakRef to avoid retaining information that will interfere with the test
28+
signalRef = new WeakRef(AbortSignal.any([ac.signal]));
29+
signalRef.deref().addEventListener('abort', handler);
30+
}
31+
32+
dependantSymbol ??= Object.getOwnPropertySymbols(ac.signal).find(
33+
(s) => s.toString() === 'Symbol(kDependantSignals)'
34+
);
35+
36+
setImmediate(() => {
37+
// Removing the event listener at some moment in the future
38+
// Which will then allow the signal to be GCed
39+
signalRef?.deref()?.removeEventListener('abort', handler);
40+
run(iteration + 1);
41+
});
42+
}
43+
44+
run(1);
45+
};
46+
47+
function runShortLivedSourceSignal(limit, done) {
48+
const signalRefs = new Set();
49+
50+
function run(iteration) {
51+
if (iteration > limit) {
52+
global.gc();
53+
done(signalRefs);
54+
return;
55+
}
56+
57+
const ac = new AbortController();
58+
signalRefs.add(new WeakRef(ac.signal));
59+
AbortSignal.any([ac.signal]);
60+
61+
setImmediate(() => run(iteration + 1));
62+
}
63+
64+
run(1);
65+
};
66+
67+
const limit = 10_000;
68+
69+
describe('when there is a long-lived signal', () => {
70+
it('drops settled dependant signals', (t, done) => {
71+
makeSubsequentCalls(limit, (signal, depandantSignalsKey) => {
72+
setImmediate(() => {
73+
t.assert.strictEqual(signal[depandantSignalsKey].size, 0);
74+
done();
75+
});
76+
});
77+
});
78+
79+
it('keeps all active dependant signals', (t, done) => {
80+
makeSubsequentCalls(limit, (signal, depandantSignalsKey) => {
81+
t.assert.strictEqual(signal[depandantSignalsKey].size, limit);
82+
83+
done();
84+
}, true);
85+
});
86+
});
87+
88+
it('does not prevent source signal from being GCed if it is short-lived', (t, done) => {
89+
runShortLivedSourceSignal(limit, (signalRefs) => {
90+
setImmediate(() => {
91+
const unGCedSignals = [...signalRefs].filter((ref) => ref.deref());
92+
93+
t.assert.strictEqual(unGCedSignals.length, 0);
94+
done();
95+
});
96+
});
97+
});
98+
99+
it('drops settled dependant signals when signal is composite', (t, done) => {
100+
const controllers = Array.from({ length: 2 }, () => new AbortController());
101+
const composedSignal1 = AbortSignal.any([controllers[0].signal]);
102+
const composedSignalRef = new WeakRef(AbortSignal.any([composedSignal1, controllers[1].signal]));
103+
104+
const kDependantSignals = Object.getOwnPropertySymbols(controllers[0].signal).find(
105+
(s) => s.toString() === 'Symbol(kDependantSignals)'
106+
);
107+
108+
setImmediate(() => {
109+
global.gc();
110+
111+
t.assert.strictEqual(composedSignalRef.deref(), undefined);
112+
t.assert.strictEqual(controllers[0].signal[kDependantSignals].size, 2);
113+
t.assert.strictEqual(controllers[1].signal[kDependantSignals].size, 1);
114+
115+
setImmediate(() => {
116+
t.assert.strictEqual(controllers[0].signal[kDependantSignals].size, 0);
117+
t.assert.strictEqual(controllers[1].signal[kDependantSignals].size, 0);
118+
119+
done();
120+
});
121+
});
122+
});

0 commit comments

Comments
 (0)