node/test/parallel/test-abortsignal-drop-settled-signals.mjs
James M Snell 97a3a8204c test: replace more uses of global with globalThis
PR-URL: https://github.com/nodejs/node/pull/56712
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
2025-01-25 07:23:11 +00:00

176 lines
4.9 KiB
JavaScript

// Flags: --expose_gc
//
import '../common/index.mjs';
import { gcUntil } from '../common/gc.js';
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(() => {
globalThis.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) {
globalThis.gc();
done(signalRefs);
return;
}
const ac = new AbortController();
signalRefs.add(new WeakRef(ac.signal));
AbortSignal.any([ac.signal]);
setImmediate(() => run(iteration + 1));
}
run(1);
};
function runWithOrphanListeners(limit, done) {
let composedSignalRef;
const composedSignalRefs = [];
const handler = () => { };
function run(iteration) {
const ac = new AbortController();
if (iteration > limit) {
setImmediate(() => {
globalThis.gc();
setImmediate(() => {
globalThis.gc();
done(composedSignalRefs);
});
});
return;
}
composedSignalRef = new WeakRef(AbortSignal.any([ac.signal]));
composedSignalRef.deref().addEventListener('abort', handler);
const otherComposedSignalRef = new WeakRef(AbortSignal.any([composedSignalRef.deref()]));
otherComposedSignalRef.deref().addEventListener('abort', handler);
composedSignalRefs.push(composedSignalRef, otherComposedSignalRef);
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());
// Using WeakRefs to avoid this test to retain information that will make the test fail
const composedSignal1 = new WeakRef(AbortSignal.any([controllers[0].signal]));
const composedSignalRef = new WeakRef(AbortSignal.any([composedSignal1.deref(), controllers[1].signal]));
const kDependantSignals = Object.getOwnPropertySymbols(controllers[0].signal).find(
(s) => s.toString() === 'Symbol(kDependantSignals)'
);
t.assert.strictEqual(controllers[0].signal[kDependantSignals].size, 2);
t.assert.strictEqual(controllers[1].signal[kDependantSignals].size, 1);
setImmediate(() => {
globalThis.gc({ execution: 'async' }).then(async () => {
await gcUntil('all signals are GCed', () => {
const totalDependantSignals = Math.max(
controllers[0].signal[kDependantSignals].size,
controllers[1].signal[kDependantSignals].size
);
return composedSignalRef.deref() === undefined && totalDependantSignals === 0;
});
done();
});
});
});
it('drops settled signals even when there are listeners', (t, done) => {
runWithOrphanListeners(limit, async (signalRefs) => {
await gcUntil('all signals are GCed', () => {
const unGCedSignals = [...signalRefs].filter((ref) => ref.deref());
return unGCedSignals.length === 0;
});
done();
});
});