-
Notifications
You must be signed in to change notification settings - Fork 29.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
d9f0407
commit a9473bb
Showing
2 changed files
with
145 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
test/parallel/test-abortsignal-drop-settled-signals.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
// Flags: --expose_gc | ||
// | ||
import '../common/index.mjs'; | ||
import { describe, it } from 'node:test'; | ||
|
||
function makeSubsequentCalls(limit, done, holdReferences = false) { | ||
let dependantSymbol; | ||
let signalRef; | ||
const ac = new AbortController(); | ||
const retainedSignals = []; | ||
const handler = () => { }; | ||
|
||
function run(iteration) { | ||
if (iteration > limit) { | ||
// This setImmediate is necessary to ensure that in the last iteration the remaining signal is GCed (if not | ||
// retained) | ||
setImmediate(() => { | ||
global.gc(); | ||
done(ac.signal, dependantSymbol); | ||
}); | ||
return; | ||
} | ||
|
||
if (holdReferences) { | ||
retainedSignals.push(AbortSignal.any([ac.signal])); | ||
} else { | ||
// Using a WeakRef to avoid retaining information that will interfere with the test | ||
signalRef = new WeakRef(AbortSignal.any([ac.signal])); | ||
signalRef.deref().addEventListener('abort', handler); | ||
} | ||
|
||
dependantSymbol ??= Object.getOwnPropertySymbols(ac.signal).find( | ||
(s) => s.toString() === 'Symbol(kDependantSignals)' | ||
); | ||
|
||
setImmediate(() => { | ||
// Removing the event listener at some moment in the future | ||
// Which will then allow the signal to be GCed | ||
signalRef?.deref()?.removeEventListener('abort', handler); | ||
run(iteration + 1); | ||
}); | ||
} | ||
|
||
run(1); | ||
}; | ||
|
||
function runShortLivedSourceSignal(limit, done) { | ||
const signalRefs = new Set(); | ||
|
||
function run(iteration) { | ||
if (iteration > limit) { | ||
global.gc(); | ||
done(signalRefs); | ||
return; | ||
} | ||
|
||
const ac = new AbortController(); | ||
signalRefs.add(new WeakRef(ac.signal)); | ||
AbortSignal.any([ac.signal]); | ||
|
||
setImmediate(() => run(iteration + 1)); | ||
} | ||
|
||
run(1); | ||
}; | ||
|
||
const limit = 10_000; | ||
|
||
describe('when there is a long-lived signal', () => { | ||
it('drops settled dependant signals', (t, done) => { | ||
makeSubsequentCalls(limit, (signal, depandantSignalsKey) => { | ||
setImmediate(() => { | ||
t.assert.strictEqual(signal[depandantSignalsKey].size, 0); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
|
||
it('keeps all active dependant signals', (t, done) => { | ||
makeSubsequentCalls(limit, (signal, depandantSignalsKey) => { | ||
t.assert.strictEqual(signal[depandantSignalsKey].size, limit); | ||
|
||
done(); | ||
}, true); | ||
}); | ||
}); | ||
|
||
it('does not prevent source signal from being GCed if it is short-lived', (t, done) => { | ||
runShortLivedSourceSignal(limit, (signalRefs) => { | ||
setImmediate(() => { | ||
const unGCedSignals = [...signalRefs].filter((ref) => ref.deref()); | ||
|
||
t.assert.strictEqual(unGCedSignals.length, 0); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
|
||
it('drops settled dependant signals when signal is composite', (t, done) => { | ||
const controllers = Array.from({ length: 2 }, () => new AbortController()); | ||
const composedSignal1 = AbortSignal.any([controllers[0].signal]); | ||
const composedSignalRef = new WeakRef(AbortSignal.any([composedSignal1, controllers[1].signal])); | ||
|
||
const kDependantSignals = Object.getOwnPropertySymbols(controllers[0].signal).find( | ||
(s) => s.toString() === 'Symbol(kDependantSignals)' | ||
); | ||
|
||
setImmediate(() => { | ||
global.gc(); | ||
|
||
t.assert.strictEqual(composedSignalRef.deref(), undefined); | ||
t.assert.strictEqual(controllers[0].signal[kDependantSignals].size, 2); | ||
t.assert.strictEqual(controllers[1].signal[kDependantSignals].size, 1); | ||
|
||
setImmediate(() => { | ||
t.assert.strictEqual(controllers[0].signal[kDependantSignals].size, 0); | ||
t.assert.strictEqual(controllers[1].signal[kDependantSignals].size, 0); | ||
|
||
done(); | ||
}); | ||
}); | ||
}); |