Skip to content

Commit c2a2673

Browse files
committed
timers: do not retain a reference to the async store after firing
After firing timers, clean up the async context references held by timer resources so AsyncLocalStorage stores can be collected. Also clear callbacks and arguments when timeouts or immediates are explicitly cleared. This covers timeouts, intervals, and immediates with and without AsyncContextFrame. Fixes: #53408 Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 665daeb commit c2a2673

5 files changed

Lines changed: 273 additions & 21 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: 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;
@@ -498,14 +537,22 @@ function getTimerCallbacks(runNextTicks) {
498537
const asyncId = immediate[async_id_symbol];
499538
emitBefore(asyncId, immediate[trigger_async_id_symbol], immediate);
500539

540+
let threw = true;
501541
try {
502542
const argv = immediate._argv;
503543
if (!argv)
504544
immediate._onImmediate();
505545
else
506546
immediate._onImmediate(...argv);
547+
threw = false;
507548
} finally {
508-
immediate._onImmediate = null;
549+
if (threw) {
550+
immediate._onImmediate = undefined;
551+
immediate._argv = undefined;
552+
enqueueRemoveStoresFromResource(immediate);
553+
} else {
554+
cleanImmediate(immediate);
555+
}
509556

510557
emitDestroy(asyncId);
511558

@@ -577,6 +624,8 @@ function getTimerCallbacks(runNextTicks) {
577624
if (!timer._destroyed) {
578625
timer._destroyed = true;
579626

627+
cleanTimer(timer);
628+
580629
if (timer[kHasPrimitive])
581630
delete knownTimersById[asyncId];
582631

@@ -599,26 +648,42 @@ function getTimerCallbacks(runNextTicks) {
599648
start = binding.getLibuvNow();
600649
}
601650

651+
let threw = true;
602652
try {
603653
const args = timer._timerArgs;
604654
if (args === undefined)
605655
timer._onTimeout();
606656
else
607657
ReflectApply(timer._onTimeout, timer, args);
658+
threw = false;
608659
} finally {
609660
if (timer._repeat && timer._idleTimeout !== -1) {
610661
timer._idleTimeout = timer._repeat;
611662
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);
663+
} else if (!timer._idleNext && !timer._idlePrev) {
664+
if (timer._destroyed) {
665+
timer._onTimeout = undefined;
666+
timer._timerArgs = undefined;
667+
if (threw)
668+
enqueueRemoveStoresIfNotReinserted(timer);
669+
else
670+
removeStoresFromResource(timer);
671+
} else {
672+
if (threw)
673+
enqueueRemoveStoresIfNotReinserted(timer);
674+
else
675+
removeStoresFromResource(timer);
676+
677+
timer._destroyed = true;
678+
679+
if (timer[kHasPrimitive])
680+
delete knownTimersById[asyncId];
681+
682+
if (timer[kRefed])
683+
timeoutInfo[0]--;
684+
685+
emitDestroy(asyncId);
686+
}
622687
}
623688
}
624689

@@ -698,8 +763,11 @@ module.exports = {
698763
kTimeout: Symbol('timeout'), // For hiding Timeouts on other internals.
699764
async_id_symbol,
700765
trigger_async_id_symbol,
766+
async_context_frame,
701767
Timeout,
702768
Immediate,
769+
cleanImmediate,
770+
cleanTimer,
703771
kRefed,
704772
kHasPrimitive,
705773
initAsyncResource,

lib/timers.js

Lines changed: 11 additions & 1 deletion
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
}
@@ -239,6 +247,8 @@ function clearImmediate(immediate) {
239247
emitDestroy(immediate[async_id_symbol]);
240248

241249
immediate._onImmediate = null;
250+
immediate._argv = undefined;
251+
cleanImmediate(immediate);
242252

243253
immediateQueue.remove(immediate);
244254
}

0 commit comments

Comments
 (0)