Skip to content

Commit b83e431

Browse files
committed
timers: do not retain a reference to the async store after firing
After firing timers, we can clean them up by iterating over all active stores and setting the relevant symbols to undefined. This also covers immediates and intervals. Refs: #53408 PR-URL: #53443 Fixes: #53408 Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 81c4f0a commit b83e431

5 files changed

Lines changed: 219 additions & 22 deletions

File tree

lib/internal/async_hooks.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const before_symbol = Symbol('before');
105105
const after_symbol = Symbol('after');
106106
const destroy_symbol = Symbol('destroy');
107107
const promise_resolve_symbol = Symbol('promiseResolve');
108+
const async_local_storage_context_symbol = Symbol('kAsyncLocalStorageContext');
108109
const emitBeforeNative = emitHookFactory(before_symbol, 'emitBeforeNative');
109110
const emitAfterNative = emitHookFactory(after_symbol, 'emitAfterNative');
110111
const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative');
@@ -595,7 +596,8 @@ module.exports = {
595596
symbols: {
596597
async_id_symbol, trigger_async_id_symbol,
597598
init_symbol, before_symbol, after_symbol, destroy_symbol,
598-
promise_resolve_symbol, owner_symbol,
599+
promise_resolve_symbol, async_local_storage_context_symbol,
600+
owner_symbol,
599601
},
600602
constants: {
601603
kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,

lib/internal/async_local_storage/async_hooks.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const {
1212
const {
1313
validateObject,
1414
} = require('internal/validators');
15+
const {
16+
symbols: {
17+
async_local_storage_context_symbol,
18+
},
19+
} = require('internal/async_hooks');
1520

1621
const {
1722
AsyncResource,
@@ -23,6 +28,11 @@ const RunScope = require('internal/async_local_storage/run_scope');
2328
const { kEmptyObject } = require('internal/util');
2429

2530
const storageList = [];
31+
32+
function getOrCreateResourceStore(resource) {
33+
return resource[async_local_storage_context_symbol] ??= { __proto__: null };
34+
}
35+
2636
const storageHook = createHook({
2737
init(asyncId, type, triggerAsyncId, resource) {
2838
const currentResource = executionAsyncResource();
@@ -91,16 +101,18 @@ class AsyncLocalStorage {
91101

92102
// Propagate the context from a parent resource to a child one
93103
_propagate(resource, triggerResource, type) {
94-
const store = triggerResource[this.kResourceStore];
104+
const store = triggerResource[async_local_storage_context_symbol]?.[this.kResourceStore];
95105
if (this.enabled) {
96-
resource[this.kResourceStore] = store;
106+
const resourceStore = getOrCreateResourceStore(resource);
107+
resourceStore[this.kResourceStore] = store;
97108
}
98109
}
99110

100111
enterWith(store) {
101112
this._enable();
102113
const resource = executionAsyncResource();
103-
resource[this.kResourceStore] = store;
114+
const resourceStore = getOrCreateResourceStore(resource);
115+
resourceStore[this.kResourceStore] = store;
104116
}
105117

106118
run(store, callback, ...args) {
@@ -112,14 +124,15 @@ class AsyncLocalStorage {
112124
this._enable();
113125

114126
const resource = executionAsyncResource();
115-
const oldStore = resource[this.kResourceStore];
127+
const resourceStore = getOrCreateResourceStore(resource);
128+
const oldStore = resourceStore[this.kResourceStore];
116129

117-
resource[this.kResourceStore] = store;
130+
resourceStore[this.kResourceStore] = store;
118131

119132
try {
120133
return ReflectApply(callback, null, args);
121134
} finally {
122-
resource[this.kResourceStore] = oldStore;
135+
resourceStore[this.kResourceStore] = oldStore;
123136
}
124137
}
125138

@@ -138,10 +151,11 @@ class AsyncLocalStorage {
138151
getStore() {
139152
if (this.enabled) {
140153
const resource = executionAsyncResource();
141-
if (!(this.kResourceStore in resource)) {
154+
const resourceStore = resource[async_local_storage_context_symbol];
155+
if (resourceStore === undefined || !(this.kResourceStore in resourceStore)) {
142156
return this.#defaultValue;
143157
}
144-
return resource[this.kResourceStore];
158+
return resourceStore[this.kResourceStore];
145159
}
146160
return this.#defaultValue;
147161
}

lib/internal/timers.js

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ const {
8787
immediateInfo,
8888
timeoutInfo,
8989
} = binding;
90+
const {
91+
enqueueMicrotask,
92+
} = internalBinding('task_queue');
9093

9194
const {
9295
getDefaultTriggerAsyncId,
@@ -97,6 +100,9 @@ const {
97100
emitBefore,
98101
emitAfter,
99102
emitDestroy,
103+
symbols: {
104+
async_local_storage_context_symbol,
105+
},
100106
} = require('internal/async_hooks');
101107

102108
// Symbols for storing async id state.
@@ -125,6 +131,36 @@ const AsyncContextFrame = require('internal/async_context_frame');
125131

126132
const async_context_frame = Symbol('kAsyncContextFrame');
127133

134+
function removeStoresFromResource(resource) {
135+
if (!AsyncContextFrame.enabled &&
136+
resource[async_local_storage_context_symbol] !== undefined) {
137+
resource[async_local_storage_context_symbol] = undefined;
138+
}
139+
}
140+
141+
function cleanTimer(timer) {
142+
removeStoresFromResource(timer);
143+
timer._onTimeout = undefined;
144+
timer._timerArgs = undefined;
145+
}
146+
147+
function cleanImmediate(immediate) {
148+
removeStoresFromResource(immediate);
149+
immediate._onImmediate = undefined;
150+
immediate._argv = undefined;
151+
}
152+
153+
function enqueueRemoveStoresFromResource(resource) {
154+
enqueueMicrotask(() => removeStoresFromResource(resource));
155+
}
156+
157+
function enqueueRemoveStoresIfNotReinserted(resource) {
158+
enqueueMicrotask(() => {
159+
if (!resource._idleNext && !resource._idlePrev)
160+
removeStoresFromResource(resource);
161+
});
162+
}
163+
128164
// *Must* match Environment::ImmediateInfo::Fields in src/env.h.
129165
const kCount = 0;
130166
const kRefCount = 1;
@@ -498,14 +534,22 @@ function getTimerCallbacks(runNextTicks) {
498534
const asyncId = immediate[async_id_symbol];
499535
emitBefore(asyncId, immediate[trigger_async_id_symbol], immediate);
500536

537+
let threw = true;
501538
try {
502539
const argv = immediate._argv;
503540
if (!argv)
504541
immediate._onImmediate();
505542
else
506543
immediate._onImmediate(...argv);
544+
threw = false;
507545
} finally {
508-
immediate._onImmediate = null;
546+
if (threw) {
547+
immediate._onImmediate = undefined;
548+
immediate._argv = undefined;
549+
enqueueRemoveStoresFromResource(immediate);
550+
} else {
551+
cleanImmediate(immediate);
552+
}
509553

510554
emitDestroy(asyncId);
511555

@@ -577,6 +621,8 @@ function getTimerCallbacks(runNextTicks) {
577621
if (!timer._destroyed) {
578622
timer._destroyed = true;
579623

624+
cleanTimer(timer);
625+
580626
if (timer[kHasPrimitive])
581627
delete knownTimersById[asyncId];
582628

@@ -599,26 +645,42 @@ function getTimerCallbacks(runNextTicks) {
599645
start = binding.getLibuvNow();
600646
}
601647

648+
let threw = true;
602649
try {
603650
const args = timer._timerArgs;
604651
if (args === undefined)
605652
timer._onTimeout();
606653
else
607654
ReflectApply(timer._onTimeout, timer, args);
655+
threw = false;
608656
} finally {
609657
if (timer._repeat && timer._idleTimeout !== -1) {
610658
timer._idleTimeout = timer._repeat;
611659
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);
660+
} else if (!timer._idleNext && !timer._idlePrev) {
661+
if (timer._destroyed) {
662+
timer._onTimeout = undefined;
663+
timer._timerArgs = undefined;
664+
if (threw)
665+
enqueueRemoveStoresIfNotReinserted(timer);
666+
else
667+
removeStoresFromResource(timer);
668+
} else {
669+
if (threw)
670+
enqueueRemoveStoresIfNotReinserted(timer);
671+
else
672+
removeStoresFromResource(timer);
673+
674+
timer._destroyed = true;
675+
676+
if (timer[kHasPrimitive])
677+
delete knownTimersById[asyncId];
678+
679+
if (timer[kRefed])
680+
timeoutInfo[0]--;
681+
682+
emitDestroy(asyncId);
683+
}
622684
}
623685
}
624686

@@ -703,6 +765,8 @@ module.exports = {
703765
kRefed,
704766
kHasPrimitive,
705767
initAsyncResource,
768+
cleanImmediate,
769+
cleanTimer,
706770
setUnrefTimeout,
707771
getTimerDuration,
708772
immediateQueue,

lib/timers.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const {
3838
async_id_symbol,
3939
Timeout,
4040
Immediate,
41+
cleanImmediate,
42+
cleanTimer,
4143
decRefCount,
4244
immediateInfoFields: {
4345
kCount,
@@ -69,9 +71,12 @@ const {
6971

7072
// Remove a timer. Cancels the timeout and resets the relevant timer properties.
7173
function unenroll(item) {
72-
if (item._destroyed)
74+
if (item._destroyed) {
75+
cleanTimer(item);
7376
return;
77+
}
7478

79+
const wasEnrolled = item._idleNext !== null || item._idlePrev !== null;
7580
item._destroyed = true;
7681

7782
if (item[kHasPrimitive])
@@ -99,6 +104,9 @@ function unenroll(item) {
99104
decRefCount();
100105
}
101106

107+
if (wasEnrolled)
108+
cleanTimer(item);
109+
102110
// If active is called later, then we want to make sure not to insert again
103111
item._idleTimeout = -1;
104112
}
@@ -238,7 +246,7 @@ function clearImmediate(immediate) {
238246

239247
emitDestroy(immediate[async_id_symbol]);
240248

241-
immediate._onImmediate = null;
249+
cleanImmediate(immediate);
242250

243251
immediateQueue.remove(immediate);
244252
}

0 commit comments

Comments
 (0)