Skip to content

Commit ff9bebb

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. Fixes #53408 Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 1709394 commit ff9bebb

5 files changed

Lines changed: 281 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');
@@ -584,7 +585,8 @@ module.exports = {
584585
symbols: {
585586
async_id_symbol, trigger_async_id_symbol,
586587
init_symbol, before_symbol, after_symbol, destroy_symbol,
587-
promise_resolve_symbol, owner_symbol,
588+
promise_resolve_symbol, async_local_storage_context_symbol,
589+
owner_symbol,
588590
},
589591
constants: {
590592
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: 79 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,39 @@ 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+
if (resource[async_context_frame] !== undefined) {
137+
resource[async_context_frame] = undefined;
138+
}
139+
} else if (resource[async_local_storage_context_symbol] !== undefined) {
140+
resource[async_local_storage_context_symbol] = undefined;
141+
}
142+
}
143+
144+
function cleanTimer(timer) {
145+
removeStoresFromResource(timer);
146+
timer._onTimeout = undefined;
147+
timer._timerArgs = undefined;
148+
}
149+
150+
function cleanImmediate(immediate) {
151+
removeStoresFromResource(immediate);
152+
immediate._onImmediate = undefined;
153+
immediate._argv = undefined;
154+
}
155+
156+
function enqueueRemoveStoresFromResource(resource) {
157+
enqueueMicrotask(() => removeStoresFromResource(resource));
158+
}
159+
160+
function enqueueRemoveStoresIfNotReinserted(resource) {
161+
enqueueMicrotask(() => {
162+
if (!resource._idleNext && !resource._idlePrev)
163+
removeStoresFromResource(resource);
164+
});
165+
}
166+
128167
// *Must* match Environment::ImmediateInfo::Fields in src/env.h.
129168
const kCount = 0;
130169
const kRefCount = 1;
@@ -528,14 +567,22 @@ function getTimerCallbacks(runNextTicks) {
528567
const asyncId = immediate[async_id_symbol];
529568
emitBefore(asyncId, immediate[trigger_async_id_symbol], immediate);
530569

570+
let threw = true;
531571
try {
532572
const argv = immediate._argv;
533573
if (!argv)
534574
immediate._onImmediate();
535575
else
536576
immediate._onImmediate(...argv);
577+
threw = false;
537578
} finally {
538-
immediate._onImmediate = null;
579+
if (threw) {
580+
immediate._onImmediate = undefined;
581+
immediate._argv = undefined;
582+
enqueueRemoveStoresFromResource(immediate);
583+
} else {
584+
cleanImmediate(immediate);
585+
}
539586

540587
emitDestroy(asyncId);
541588

@@ -607,6 +654,8 @@ function getTimerCallbacks(runNextTicks) {
607654
if (!timer._destroyed) {
608655
timer._destroyed = true;
609656

657+
cleanTimer(timer);
658+
610659
if (timer[kHasPrimitive])
611660
delete knownTimersById[asyncId];
612661

@@ -629,26 +678,42 @@ function getTimerCallbacks(runNextTicks) {
629678
start = binding.getLibuvNow();
630679
}
631680

681+
let threw = true;
632682
try {
633683
const args = timer._timerArgs;
634684
if (args === undefined)
635685
timer._onTimeout();
636686
else
637687
ReflectApply(timer._onTimeout, timer, args);
688+
threw = false;
638689
} finally {
639690
if (timer._repeat && timer._idleTimeout !== -1) {
640691
timer._idleTimeout = timer._repeat;
641692
insert(timer, timer._idleTimeout, start);
642-
} else if (!timer._idleNext && !timer._idlePrev && !timer._destroyed) {
643-
timer._destroyed = true;
644-
645-
if (timer[kHasPrimitive])
646-
delete knownTimersById[asyncId];
647-
648-
if (timer[kRefed])
649-
timeoutInfo[0]--;
650-
651-
emitDestroy(asyncId);
693+
} else if (!timer._idleNext && !timer._idlePrev) {
694+
if (timer._destroyed) {
695+
timer._onTimeout = undefined;
696+
timer._timerArgs = undefined;
697+
if (threw)
698+
enqueueRemoveStoresIfNotReinserted(timer);
699+
else
700+
removeStoresFromResource(timer);
701+
} else {
702+
if (threw)
703+
enqueueRemoveStoresIfNotReinserted(timer);
704+
else
705+
removeStoresFromResource(timer);
706+
707+
timer._destroyed = true;
708+
709+
if (timer[kHasPrimitive])
710+
delete knownTimersById[asyncId];
711+
712+
if (timer[kRefed])
713+
timeoutInfo[0]--;
714+
715+
emitDestroy(asyncId);
716+
}
652717
}
653718
}
654719

@@ -728,8 +793,11 @@ module.exports = {
728793
kTimeout: Symbol('timeout'), // For hiding Timeouts on other internals.
729794
async_id_symbol,
730795
trigger_async_id_symbol,
796+
async_context_frame,
731797
Timeout,
732798
Immediate,
799+
cleanImmediate,
800+
cleanTimer,
733801
kRefed,
734802
kHasPrimitive,
735803
initAsyncResource,

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+
cleanTimer,
42+
cleanImmediate,
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)