Skip to content

Commit 522e6eb

Browse files
committed
timers: fix review comments
- fix removeStoresFromResource for ACF and new ALS impl - add clearStoreFromResource to async_hooks - add cleanup to clearTimeout, clearImmediate, processImmediate, listOnTimeout - export cleanTimer/cleanImmediate from internal/timers - update test for ACF and non-ACF variants, add GC test Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 665daeb commit 522e6eb

4 files changed

Lines changed: 214 additions & 11 deletions

File tree

lib/internal/async_local_storage/async_hooks.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ const RunScope = require('internal/async_local_storage/run_scope');
2323
const { kEmptyObject } = require('internal/util');
2424

2525
const storageList = [];
26+
27+
function clearStoreFromResource(resource) {
28+
for (let i = 0; i < storageList.length; ++i) {
29+
resource[storageList[i].kResourceStore] = undefined;
30+
}
31+
}
32+
2633
const storageHook = createHook({
2734
init(asyncId, type, triggerAsyncId, resource) {
2835
const currentResource = executionAsyncResource();
@@ -152,3 +159,4 @@ class AsyncLocalStorage {
152159
}
153160

154161
module.exports = AsyncLocalStorage;
162+
module.exports.clearStoreFromResource = clearStoreFromResource;

lib/internal/timers.js

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,38 @@ const AsyncContextFrame = require('internal/async_context_frame');
125125

126126
const async_context_frame = Symbol('kAsyncContextFrame');
127127

128+
function removeStoresFromResource(resource) {
129+
if (AsyncContextFrame.enabled) {
130+
if (resource[async_context_frame] !== undefined) {
131+
resource[async_context_frame] = undefined;
132+
}
133+
} else {
134+
require('internal/async_local_storage/async_hooks').clearStoreFromResource(resource);
135+
}
136+
}
137+
138+
function cleanTimer(timer) {
139+
removeStoresFromResource(timer);
140+
timer._onTimeout = undefined;
141+
timer._timerArgs = undefined;
142+
}
143+
144+
function cleanImmediate(immediate) {
145+
removeStoresFromResource(immediate);
146+
immediate._onImmediate = undefined;
147+
immediate._argv = undefined;
148+
}
149+
150+
function enqueueRemoveStoresFromResource(resource) {
151+
enqueueMicrotask(() => removeStoresFromResource(resource));
152+
}
153+
154+
function enqueueRemoveStoresIfNotReinserted(resource) {
155+
enqueueMicrotask(() => {
156+
if (!resource._idleNext && !resource._idlePrev)
157+
removeStoresFromResource(resource);
158+
});
159+
}
128160
// *Must* match Environment::ImmediateInfo::Fields in src/env.h.
129161
const kCount = 0;
130162
const kRefCount = 1;
@@ -498,14 +530,22 @@ function getTimerCallbacks(runNextTicks) {
498530
const asyncId = immediate[async_id_symbol];
499531
emitBefore(asyncId, immediate[trigger_async_id_symbol], immediate);
500532

533+
let threw = true;
501534
try {
502535
const argv = immediate._argv;
503536
if (!argv)
504537
immediate._onImmediate();
505538
else
506539
immediate._onImmediate(...argv);
540+
threw = false;
507541
} finally {
508-
immediate._onImmediate = null;
542+
if (threw) {
543+
immediate._onImmediate = undefined;
544+
immediate._argv = undefined;
545+
enqueueRemoveStoresFromResource(immediate);
546+
} else {
547+
cleanImmediate(immediate);
548+
}
509549

510550
emitDestroy(asyncId);
511551

@@ -599,26 +639,42 @@ function getTimerCallbacks(runNextTicks) {
599639
start = binding.getLibuvNow();
600640
}
601641

642+
let threw = true;
602643
try {
603644
const args = timer._timerArgs;
604645
if (args === undefined)
605646
timer._onTimeout();
606647
else
607648
ReflectApply(timer._onTimeout, timer, args);
649+
threw = false;
608650
} finally {
609651
if (timer._repeat && timer._idleTimeout !== -1) {
610652
timer._idleTimeout = timer._repeat;
611653
insert(timer, timer._idleTimeout, start);
612-
} else if (!timer._idleNext && !timer._idlePrev && !timer._destroyed) {
613-
timer._destroyed = true;
614-
615-
if (timer[kHasPrimitive])
616-
delete knownTimersById[asyncId];
617-
618-
if (timer[kRefed])
619-
timeoutInfo[0]--;
620-
621-
emitDestroy(asyncId);
654+
} else if (!timer._idleNext && !timer._idlePrev) {
655+
if (timer._destroyed) {
656+
timer._onTimeout = undefined;
657+
timer._timerArgs = undefined;
658+
if (threw)
659+
enqueueRemoveStoresIfNotReinserted(timer);
660+
else
661+
removeStoresFromResource(timer);
662+
} else {
663+
if (threw)
664+
enqueueRemoveStoresIfNotReinserted(timer);
665+
else
666+
removeStoresFromResource(timer);
667+
668+
timer._destroyed = true;
669+
670+
if (timer[kHasPrimitive])
671+
delete knownTimersById[asyncId];
672+
673+
if (timer[kRefed])
674+
timeoutInfo[0]--;
675+
676+
emitDestroy(asyncId);
677+
}
622678
}
623679
}
624680

@@ -698,8 +754,11 @@ module.exports = {
698754
kTimeout: Symbol('timeout'), // For hiding Timeouts on other internals.
699755
async_id_symbol,
700756
trigger_async_id_symbol,
757+
async_context_frame,
701758
Timeout,
702759
Immediate,
760+
cleanImmediate,
761+
cleanTimer,
703762
kRefed,
704763
kHasPrimitive,
705764
initAsyncResource,

lib/timers.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const {
3838
async_id_symbol,
3939
Timeout,
4040
Immediate,
41+
cleanTimer,
42+
cleanImmediate,
4143
decRefCount,
4244
immediateInfoFields: {
4345
kCount,
@@ -136,13 +138,17 @@ ObjectDefineProperty(setTimeout, customPromisify, {
136138
function clearTimeout(timer) {
137139
if (timer?._onTimeout) {
138140
timer._onTimeout = null;
141+
timer._timerArgs = undefined;
142+
cleanTimer(timer);
139143
unenroll(timer);
140144
return;
141145
}
142146
if (typeof timer === 'number' || typeof timer === 'string') {
143147
const timerInstance = knownTimersById[timer];
144148
if (timerInstance !== undefined) {
145149
timerInstance._onTimeout = null;
150+
timerInstance._timerArgs = undefined;
151+
cleanTimer(timerInstance);
146152
unenroll(timerInstance);
147153
}
148154
}
@@ -239,6 +245,8 @@ function clearImmediate(immediate) {
239245
emitDestroy(immediate[async_id_symbol]);
240246

241247
immediate._onImmediate = null;
248+
immediate._argv = undefined;
249+
cleanImmediate(immediate);
242250

243251
immediateQueue.remove(immediate);
244252
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Flags: --expose-internals --expose-gc
2+
'use strict';
3+
4+
const common = require('../common');
5+
const { AsyncLocalStorage } = require('async_hooks');
6+
const assert = require('assert');
7+
8+
const AsyncContextFrame = require('internal/async_context_frame');
9+
const async_context_frame = require('internal/timers').async_context_frame;
10+
11+
// When async-context-frame is enabled, stores are stored in the async context
12+
// frame, not directly on the resource. The resource holds a reference to the
13+
// frame via async_context_frame.
14+
const isACFEnabled = AsyncContextFrame.enabled;
15+
16+
function assertNoStore(resource, als) {
17+
if (isACFEnabled) {
18+
assert.strictEqual(resource[async_context_frame], undefined);
19+
} else {
20+
assert.strictEqual(resource[als.kResourceStore], undefined);
21+
}
22+
}
23+
24+
// Test that setTimeout does not retain a reference to the async store after
25+
// firing. The callback and arguments must stay on the Timeout object so that
26+
// refresh() can reactivate the timer.
27+
{
28+
const asyncLocalStorage = new AsyncLocalStorage();
29+
const store = {};
30+
const arg = {};
31+
asyncLocalStorage.run(store, common.mustCall(() => {
32+
const timeout = setTimeout(common.mustCall((received) => {
33+
assert.strictEqual(received, arg);
34+
setImmediate(common.mustCall(() => {
35+
assertNoStore(timeout, asyncLocalStorage);
36+
}));
37+
}), 1, arg);
38+
assert.strictEqual(asyncLocalStorage.getStore(), store);
39+
}));
40+
}
41+
42+
// Test that clearTimeout cleans up the store, callback, and arguments before
43+
// firing.
44+
{
45+
const asyncLocalStorage = new AsyncLocalStorage();
46+
const store = {};
47+
const arg = {};
48+
asyncLocalStorage.run(store, common.mustCall(() => {
49+
const timeout = setTimeout(common.mustNotCall(), common.platformTimeout(1000), arg);
50+
assert.strictEqual(asyncLocalStorage.getStore(), store);
51+
clearTimeout(timeout);
52+
assertNoStore(timeout, asyncLocalStorage);
53+
assert.strictEqual(timeout._onTimeout, undefined);
54+
assert.strictEqual(timeout._timerArgs, undefined);
55+
}));
56+
}
57+
58+
// Test that setInterval does not retain a reference to the async store,
59+
// callback, or arguments after it is cleared.
60+
{
61+
const asyncLocalStorage = new AsyncLocalStorage();
62+
const store = {};
63+
const arg = {};
64+
asyncLocalStorage.run(store, common.mustCall(() => {
65+
const interval = setInterval(common.mustCall((received) => {
66+
assert.strictEqual(received, arg);
67+
assert.strictEqual(asyncLocalStorage.getStore(), store);
68+
clearInterval(interval);
69+
setImmediate(common.mustCall(() => {
70+
assertNoStore(interval, asyncLocalStorage);
71+
assert.strictEqual(interval._onTimeout, undefined);
72+
assert.strictEqual(interval._timerArgs, undefined);
73+
}));
74+
}), 1, arg);
75+
assert.strictEqual(asyncLocalStorage.getStore(), store);
76+
}));
77+
}
78+
79+
// Test that setImmediate does not retain a reference to the async store,
80+
// callback, or arguments after firing.
81+
{
82+
const asyncLocalStorage = new AsyncLocalStorage();
83+
const store = {};
84+
const arg = {};
85+
asyncLocalStorage.run(store, common.mustCall(() => {
86+
const immediate = setImmediate(common.mustCall((received) => {
87+
assert.strictEqual(received, arg);
88+
setImmediate(common.mustCall(() => {
89+
assertNoStore(immediate, asyncLocalStorage);
90+
assert.strictEqual(immediate._onImmediate, undefined);
91+
assert.strictEqual(immediate._argv, undefined);
92+
}));
93+
}), arg);
94+
assert.strictEqual(asyncLocalStorage.getStore(), store);
95+
}));
96+
}
97+
98+
// Test that clearImmediate cleans up the store, callback, and arguments before
99+
// firing.
100+
{
101+
const asyncLocalStorage = new AsyncLocalStorage();
102+
const store = {};
103+
const arg = {};
104+
asyncLocalStorage.run(store, common.mustCall(() => {
105+
const immediate = setImmediate(common.mustNotCall(), arg);
106+
assert.strictEqual(asyncLocalStorage.getStore(), store);
107+
clearImmediate(immediate);
108+
assertNoStore(immediate, asyncLocalStorage);
109+
assert.strictEqual(immediate._onImmediate, undefined);
110+
assert.strictEqual(immediate._argv, undefined);
111+
}));
112+
}
113+
114+
// Test that the async store reference is cleaned up and can be GC'd.
115+
{
116+
const asyncLocalStorage = new AsyncLocalStorage();
117+
let timeout;
118+
const weakStore = new WeakRef({});
119+
asyncLocalStorage.run(weakStore.deref(), common.mustCall(() => {
120+
timeout = setTimeout(common.mustNotCall(), common.platformTimeout(1000));
121+
assert.strictEqual(weakStore.deref() !== undefined, true);
122+
clearTimeout(timeout);
123+
}));
124+
setImmediate(common.mustCall(() => {
125+
global.gc();
126+
assert.strictEqual(weakStore.deref(), undefined);
127+
}));
128+
}

0 commit comments

Comments
 (0)