diff --git a/lib/async_hooks.js b/lib/async_hooks.js index c352bff6d282a9..b170a00212b7b1 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -381,4 +381,8 @@ module.exports = { asyncWrapProviders: ObjectFreeze({ __proto__: null, ...asyncWrap.Providers }), // Embedder API AsyncResource, + // TODO(mcollina): make the list private again. + getActiveStores () { + return [...storageList]; + } }; diff --git a/lib/internal/timers.js b/lib/internal/timers.js index 5c56cf106350c1..69e4f86e2d61a0 100644 --- a/lib/internal/timers.js +++ b/lib/internal/timers.js @@ -121,6 +121,8 @@ let debug = require('internal/util/debuglog').debuglog('timer', (fn) => { debug = fn; }); +let asyncHooks = null; + // *Must* match Environment::ImmediateInfo::Fields in src/env.h. const kCount = 0; const kRefCount = 1; @@ -164,6 +166,7 @@ function initAsyncResource(resource, type) { if (initHooksExist()) emitInit(asyncId, type, triggerAsyncId, resource); } + class Timeout { // Timer constructor function. // The entire prototype is defined in lib/timers.js @@ -429,6 +432,20 @@ function setPosition(node, pos) { node.priorityQueuePosition = pos; } +function removeAllStores (timer) { + // TODO(mcollina): move the list of stores to private + asyncHooks ??= require('async_hooks'); + + // TODO(mcollina): this does a copy for safety, we + // should be fast and unsafe. + const activeStores = asyncHooks.getActiveStores(); + + // Use for loop for speed + for (let i = 0; i < activeStores.length; i++) { + timer[activeStores[i].kResourceStore] = undefined; + } +} + function getTimerCallbacks(runNextTicks) { // If an uncaught exception was thrown during execution of immediateQueue, // this queue will store all remaining Immediates that need to run upon @@ -594,6 +611,9 @@ function getTimerCallbacks(runNextTicks) { if (timer[kRefed]) timeoutInfo[0]--; + removeAllStores(timer); + timer._onTimeout = undefined; + if (destroyHooksExist()) emitDestroy(asyncId); } diff --git a/test/parallel/timeout-async-store-leak.js b/test/parallel/timeout-async-store-leak.js new file mode 100644 index 00000000000000..70be8a004a3d9b --- /dev/null +++ b/test/parallel/timeout-async-store-leak.js @@ -0,0 +1,15 @@ +'use strict'; + +const common = require('../common'); +const { AsyncLocalStorage } = require('async_hooks'); +const assert = require('assert'); + +const asyncLocalStorage = new AsyncLocalStorage(); +asyncLocalStorage.run({}, common.mustCall(() => { + const timeout = setTimeout(common.mustCall(() => { + setImmediate(common.mustCall(() => { + assert.strictEqual(timeout[asyncLocalStorage.kResourceStore], undefined); + assert.strictEqual(timeout._onTimeout, undefined); + })) + })); +}));