From 8752332d0d7785b9d765af9c33ba9be475358414 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 4 May 2018 08:00:07 +0200 Subject: [PATCH 01/15] async_hooks: add executionAsyncResource Remove the need for the destroy hook in the basic APM case. Co-authored-by: Stephen Belanger PR-URL: /~https://github.com/nodejs/node/pull/30959 Reviewed-By: Matteo Collina Reviewed-By: Anna Henningsen Reviewed-By: Vladimir de Turckheim Reviewed-By: Chengzhong Wu Reviewed-By: James M Snell --- .../async_hooks/async-resource-vs-destroy.js | 151 ++++++++++++++++++ doc/api/async_hooks.md | 56 +++++++ lib/async_hooks.js | 4 +- lib/internal/async_hooks.js | 45 ++++-- lib/internal/process/task_queues.js | 2 +- lib/internal/timers.js | 6 +- src/api/callback.cc | 10 +- src/async_wrap.cc | 38 +++-- src/async_wrap.h | 6 +- src/env-inl.h | 30 +++- src/env.h | 8 +- src/node_internals.h | 6 +- src/node_main_instance.cc | 5 +- src/node_platform.cc | 6 +- src/node_worker.cc | 5 +- .../test-async-exec-resource-http.js | 30 ++++ test/benchmark/test-benchmark-async-hooks.js | 4 +- ...nc-hooks-execution-async-resource-await.js | 54 +++++++ ...st-async-hooks-execution-async-resource.js | 49 ++++++ 19 files changed, 458 insertions(+), 57 deletions(-) create mode 100644 benchmark/async_hooks/async-resource-vs-destroy.js create mode 100644 test/async-hooks/test-async-exec-resource-http.js create mode 100644 test/parallel/test-async-hooks-execution-async-resource-await.js create mode 100644 test/parallel/test-async-hooks-execution-async-resource.js diff --git a/benchmark/async_hooks/async-resource-vs-destroy.js b/benchmark/async_hooks/async-resource-vs-destroy.js new file mode 100644 index 00000000000000..4464dd5f93e7de --- /dev/null +++ b/benchmark/async_hooks/async-resource-vs-destroy.js @@ -0,0 +1,151 @@ +'use strict'; + +const { promisify } = require('util'); +const { readFile } = require('fs'); +const sleep = promisify(setTimeout); +const read = promisify(readFile); +const common = require('../common.js'); +const { + createHook, + executionAsyncResource, + executionAsyncId +} = require('async_hooks'); +const { createServer } = require('http'); + +// Configuration for the http server +// there is no need for parameters in this test +const connections = 500; +const path = '/'; + +const bench = common.createBenchmark(main, { + type: ['async-resource', 'destroy'], + asyncMethod: ['callbacks', 'async'], + n: [1e6] +}); + +function buildCurrentResource(getServe) { + const server = createServer(getServe(getCLS, setCLS)); + const hook = createHook({ init }); + const cls = Symbol('cls'); + hook.enable(); + + return { + server, + close + }; + + function getCLS() { + const resource = executionAsyncResource(); + if (resource === null || !resource[cls]) { + return null; + } + return resource[cls].state; + } + + function setCLS(state) { + const resource = executionAsyncResource(); + if (resource === null) { + return; + } + if (!resource[cls]) { + resource[cls] = { state }; + } else { + resource[cls].state = state; + } + } + + function init(asyncId, type, triggerAsyncId, resource) { + var cr = executionAsyncResource(); + if (cr !== null) { + resource[cls] = cr[cls]; + } + } + + function close() { + hook.disable(); + server.close(); + } +} + +function buildDestroy(getServe) { + const transactions = new Map(); + const server = createServer(getServe(getCLS, setCLS)); + const hook = createHook({ init, destroy }); + hook.enable(); + + return { + server, + close + }; + + function getCLS() { + const asyncId = executionAsyncId(); + return transactions.has(asyncId) ? transactions.get(asyncId) : null; + } + + function setCLS(value) { + const asyncId = executionAsyncId(); + transactions.set(asyncId, value); + } + + function init(asyncId, type, triggerAsyncId, resource) { + transactions.set(asyncId, getCLS()); + } + + function destroy(asyncId) { + transactions.delete(asyncId); + } + + function close() { + hook.disable(); + server.close(); + } +} + +function getServeAwait(getCLS, setCLS) { + return async function serve(req, res) { + setCLS(Math.random()); + await sleep(10); + await read(__filename); + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ cls: getCLS() })); + }; +} + +function getServeCallbacks(getCLS, setCLS) { + return function serve(req, res) { + setCLS(Math.random()); + setTimeout(() => { + readFile(__filename, () => { + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ cls: getCLS() })); + }); + }, 10); + }; +} + +const types = { + 'async-resource': buildCurrentResource, + 'destroy': buildDestroy +}; + +const asyncMethods = { + 'callbacks': getServeCallbacks, + 'async': getServeAwait +}; + +function main({ type, asyncMethod }) { + const { server, close } = types[type](asyncMethods[asyncMethod]); + + server + .listen(common.PORT) + .on('listening', () => { + + bench.http({ + path, + connections + }, () => { + close(); + }); + }); +} diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 66534ca85029e6..51792e2dd1d5be 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -459,6 +459,62 @@ init for PROMISE with id 6, trigger id: 5 # the Promise returned by then() after 6 ``` +#### `async_hooks.executionAsyncResource()` + + + +* Returns: {Object} The resource representing the current execution. + Useful to store data within the resource. + +Resource objects returned by `executionAsyncResource()` are most often internal +Node.js handle objects with undocumented APIs. Using any functions or properties +on the object is likely to crash your application and should be avoided. + +Using `executionAsyncResource()` in the top-level execution context will +return an empty object as there is no handle or request object to use, +but having an object representing the top-level can be helpful. + +```js +const { open } = require('fs'); +const { executionAsyncId, executionAsyncResource } = require('async_hooks'); + +console.log(executionAsyncId(), executionAsyncResource()); // 1 {} +open(__filename, 'r', (err, fd) => { + console.log(executionAsyncId(), executionAsyncResource()); // 7 FSReqWrap +}); +``` + +This can be used to implement continuation local storage without the +use of a tracking `Map` to store the metadata: + +```js +const { createServer } = require('http'); +const { + executionAsyncId, + executionAsyncResource, + createHook +} = require('async_hooks'); +const sym = Symbol('state'); // Private symbol to avoid pollution + +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + const cr = executionAsyncResource(); + if (cr) { + resource[sym] = cr[sym]; + } + } +}).enable(); + +const server = createServer(function(req, res) { + executionAsyncResource()[sym] = { state: req.url }; + setTimeout(function() { + res.end(JSON.stringify(executionAsyncResource()[sym])); + }, 100); +}).listen(3000); +``` + #### `async_hooks.executionAsyncId()` + +This class is used to create asynchronous state within callbacks and promise +chains. It allows storing data throughout the lifetime of a web request +or any other asynchronous duration. It is similar to thread-local storage +in other languages. + +The following example builds a logger that will always know the current HTTP +request and uses it to display enhanced logs without needing to explicitly +provide the current HTTP request to it. + +```js +const { AsyncLocalStorage } = require('async_hooks'); +const http = require('http'); + +const kReq = 'CURRENT_REQUEST'; +const asyncLocalStorage = new AsyncLocalStorage(); + +function log(...args) { + const store = asyncLocalStorage.getStore(); + // Make sure the store exists and it contains a request. + if (store && store.has(kReq)) { + const req = store.get(kReq); + // Prints `GET /items ERR could not do something + console.log(req.method, req.url, ...args); + } else { + console.log(...args); + } +} + +http.createServer((request, response) => { + asyncLocalStorage.run(() => { + const store = asyncLocalStorage.getStore(); + store.set(kReq, request); + someAsyncOperation((err, result) => { + if (err) { + log('ERR', err.message); + } + }); + }); +}) +.listen(8080); +``` + +When having multiple instances of `AsyncLocalStorage`, they are independent +from each other. It is safe to instantiate this class multiple times. + +### `new AsyncLocalStorage()` + + +Creates a new instance of `AsyncLocalStorage`. Store is only provided within a +`run` or a `runSyncAndReturn` method call. + +### `asyncLocalStorage.disable()` + + +This method disables the instance of `AsyncLocalStorage`. All subsequent calls +to `asyncLocalStorage.getStore()` will return `undefined` until +`asyncLocalStorage.run()` or `asyncLocalStorage.runSyncAndReturn()` +is called again. + +When calling `asyncLocalStorage.disable()`, all current contexts linked to the +instance will be exited. + +Calling `asyncLocalStorage.disable()` is required before the +`asyncLocalStorage` can be garbage collected. This does not apply to stores +provided by the `asyncLocalStorage`, as those objects are garbage collected +along with the corresponding async resources. + +This method is to be used when the `asyncLocalStorage` is not in use anymore +in the current process. + +### `asyncLocalStorage.getStore()` + + +* Returns: {Map} + +This method returns the current store. +If this method is called outside of an asynchronous context initialized by +calling `asyncLocalStorage.run` or `asyncLocalStorage.runAndReturn`, it will +return `undefined`. + +### `asyncLocalStorage.run(callback[, ...args])` + + +* `callback` {Function} +* `...args` {any} + +Calling `asyncLocalStorage.run(callback)` will create a new asynchronous +context. +Within the callback function and the asynchronous operations from the callback, +`asyncLocalStorage.getStore()` will return an instance of `Map` known as +"the store". This store will be persistent through the following +asynchronous calls. + +The callback will be ran asynchronously. Optionally, arguments can be passed +to the function. They will be passed to the callback function. + +If an error is thrown by the callback function, it will not be caught by +a `try/catch` block as the callback is ran in a new asynchronous resource. +Also, the stacktrace will be impacted by the asynchronous call. + +Example: + +```js +asyncLocalStorage.run(() => { + asyncLocalStorage.getStore(); // Returns a Map + someAsyncOperation(() => { + asyncLocalStorage.getStore(); // Returns the same Map + }); +}); +asyncLocalStorage.getStore(); // Returns undefined +``` + +### `asyncLocalStorage.exit(callback[, ...args])` + + +* `callback` {Function} +* `...args` {any} + +Calling `asyncLocalStorage.exit(callback)` will create a new asynchronous +context. +Within the callback function and the asynchronous operations from the callback, +`asyncLocalStorage.getStore()` will return `undefined`. + +The callback will be ran asynchronously. Optionally, arguments can be passed +to the function. They will be passed to the callback function. + +If an error is thrown by the callback function, it will not be caught by +a `try/catch` block as the callback is ran in a new asynchronous resource. +Also, the stacktrace will be impacted by the asynchronous call. + +Example: + +```js +asyncLocalStorage.run(() => { + asyncLocalStorage.getStore(); // Returns a Map + asyncLocalStorage.exit(() => { + asyncLocalStorage.getStore(); // Returns undefined + }); + asyncLocalStorage.getStore(); // Returns the same Map +}); +``` + +### `asyncLocalStorage.runSyncAndReturn(callback[, ...args])` + + +* `callback` {Function} +* `...args` {any} + +This methods runs a function synchronously within a context and return its +return value. The store is not accessible outside of the callback function or +the asynchronous operations created within the callback. + +Optionally, arguments can be passed to the function. They will be passed to +the callback function. + +If the callback function throws an error, it will be thrown by +`runSyncAndReturn` too. The stacktrace will not be impacted by this call and +the context will be exited. + +Example: + +```js +try { + asyncLocalStorage.runSyncAndReturn(() => { + asyncLocalStorage.getStore(); // Returns a Map + throw new Error(); + }); +} catch (e) { + asyncLocalStorage.getStore(); // Returns undefined + // The error will be caught here +} +``` + +### `asyncLocalStorage.exitSyncAndReturn(callback[, ...args])` + + +* `callback` {Function} +* `...args` {any} + +This methods runs a function synchronously outside of a context and return its +return value. The store is not accessible within the callback function or +the asynchronous operations created within the callback. + +Optionally, arguments can be passed to the function. They will be passed to +the callback function. + +If the callback function throws an error, it will be thrown by +`exitSyncAndReturn` too. The stacktrace will not be impacted by this call and +the context will be re-entered. + +Example: + +```js +// Within a call to run or runSyncAndReturn +try { + asyncLocalStorage.getStore(); // Returns a Map + asyncLocalStorage.exitSyncAndReturn(() => { + asyncLocalStorage.getStore(); // Returns undefined + throw new Error(); + }); +} catch (e) { + asyncLocalStorage.getStore(); // Returns the same Map + // The error will be caught here +} +``` + +### Choosing between `run` and `runSyncAndReturn` + +#### When to choose `run` + +`run` is asynchronous. It is called with a callback function that +runs within a new asynchronous call. This is the most explicit behavior as +everything that is executed within the callback of `run` (including further +asynchronous operations) will have access to the store. + +If an instance of `AsyncLocalStorage` is used for error management (for +instance, with `process.setUncaughtExceptionCaptureCallback`), only +exceptions thrown in the scope of the callback function will be associated +with the context. + +This method is the safest as it provides strong scoping and consistent +behavior. + +It cannot be promisified using `util.promisify`. If needed, the `Promise` +constructor can be used: + +```js +new Promise((resolve, reject) => { + asyncLocalStorage.run(() => { + someFunction((err, result) => { + if (err) { + return reject(err); + } + return resolve(result); + }); + }); +}); +``` + +#### When to choose `runSyncAndReturn` + +`runSyncAndReturn` is synchronous. The callback function will be executed +synchronously and its return value will be returned by `runSyncAndReturn`. +The store will only be accessible from within the callback +function and the asynchronous operations created within this scope. +If the callback throws an error, `runSyncAndReturn` will throw it and it will +not be associated with the context. + +This method provides good scoping while being synchronous. + +#### Usage with `async/await` + +If, within an async function, only one `await` call is to run within a context, +the following pattern should be used: + +```js +async function fn() { + await asyncLocalStorage.runSyncAndReturn(() => { + asyncLocalStorage.getStore().set('key', value); + return foo(); // The return value of foo will be awaited + }); +} +``` + +In this example, the store is only available in the callback function and the +functions called by `foo`. Outside of `runSyncAndReturn`, calling `getStore` +will return `undefined`. + [`after` callback]: #async_hooks_after_asyncid [`before` callback]: #async_hooks_before_asyncid [`destroy` callback]: #async_hooks_destroy_asyncid diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 3ebc9af473d5c8..23f8ddde671e30 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -1,9 +1,11 @@ 'use strict'; const { + Map, NumberIsSafeInteger, ReflectApply, Symbol, + } = primordials; const { @@ -209,11 +211,102 @@ class AsyncResource { } } +const storageList = []; +const storageHook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + const currentResource = executionAsyncResource(); + // Value of currentResource is always a non null object + for (let i = 0; i < storageList.length; ++i) { + storageList[i]._propagate(resource, currentResource); + } + } +}); + +class AsyncLocalStorage { + constructor() { + this.kResourceStore = Symbol('kResourceStore'); + this.enabled = false; + } + + disable() { + if (this.enabled) { + this.enabled = false; + // If this.enabled, the instance must be in storageList + storageList.splice(storageList.indexOf(this), 1); + if (storageList.length === 0) { + storageHook.disable(); + } + } + } + + // Propagate the context from a parent resource to a child one + _propagate(resource, triggerResource) { + const store = triggerResource[this.kResourceStore]; + if (this.enabled) { + resource[this.kResourceStore] = store; + } + } + + _enter() { + if (!this.enabled) { + this.enabled = true; + storageList.push(this); + storageHook.enable(); + } + const resource = executionAsyncResource(); + resource[this.kResourceStore] = new Map(); + } + + _exit() { + const resource = executionAsyncResource(); + if (resource) { + resource[this.kResourceStore] = undefined; + } + } + + runSyncAndReturn(callback, ...args) { + this._enter(); + try { + return callback(...args); + } finally { + this._exit(); + } + } + + exitSyncAndReturn(callback, ...args) { + this.enabled = false; + try { + return callback(...args); + } finally { + this.enabled = true; + } + } + + getStore() { + const resource = executionAsyncResource(); + if (this.enabled) { + return resource[this.kResourceStore]; + } + } + + run(callback, ...args) { + this._enter(); + process.nextTick(callback, ...args); + this._exit(); + } + + exit(callback, ...args) { + this.enabled = false; + process.nextTick(callback, ...args); + this.enabled = true; + } +} // Placing all exports down here because the exported classes won't export // otherwise. module.exports = { // Public API + AsyncLocalStorage, createHook, executionAsyncId, triggerAsyncId, diff --git a/test/async-hooks/test-async-local-storage-args.js b/test/async-hooks/test-async-local-storage-args.js new file mode 100644 index 00000000000000..91a3385e6eeb16 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-args.js @@ -0,0 +1,20 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +asyncLocalStorage.run((runArg) => { + assert.strictEqual(runArg, 1); + asyncLocalStorage.exit((exitArg) => { + assert.strictEqual(exitArg, 2); + }, 2); +}, 1); + +asyncLocalStorage.runSyncAndReturn((runArg) => { + assert.strictEqual(runArg, 'foo'); + asyncLocalStorage.exitSyncAndReturn((exitArg) => { + assert.strictEqual(exitArg, 'bar'); + }, 'bar'); +}, 'foo'); diff --git a/test/async-hooks/test-async-local-storage-async-await.js b/test/async-hooks/test-async-local-storage-async-await.js new file mode 100644 index 00000000000000..28c8488da62c53 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-async-await.js @@ -0,0 +1,19 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +async function test() { + asyncLocalStorage.getStore().set('foo', 'bar'); + await Promise.resolve(); + assert.strictEqual(asyncLocalStorage.getStore().get('foo'), 'bar'); +} + +async function main() { + await asyncLocalStorage.runSyncAndReturn(test); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +} + +main(); diff --git a/test/async-hooks/test-async-local-storage-async-functions.js b/test/async-hooks/test-async-local-storage-async-functions.js new file mode 100644 index 00000000000000..89ac0be62c7488 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-async-functions.js @@ -0,0 +1,27 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +async function foo() {} + +const asyncLocalStorage = new AsyncLocalStorage(); + +async function testOut() { + await foo(); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +} + +async function testAwait() { + await foo(); + assert.notStrictEqual(asyncLocalStorage.getStore(), undefined); + assert.strictEqual(asyncLocalStorage.getStore().get('key'), 'value'); + await asyncLocalStorage.exitSyncAndReturn(testOut); +} + +asyncLocalStorage.run(() => { + const store = asyncLocalStorage.getStore(); + store.set('key', 'value'); + testAwait(); // should not reject +}); +assert.strictEqual(asyncLocalStorage.getStore(), undefined); diff --git a/test/async-hooks/test-async-local-storage-enable-disable.js b/test/async-hooks/test-async-local-storage-enable-disable.js new file mode 100644 index 00000000000000..c30d72eb805d5d --- /dev/null +++ b/test/async-hooks/test-async-local-storage-enable-disable.js @@ -0,0 +1,21 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +asyncLocalStorage.runSyncAndReturn(() => { + asyncLocalStorage.getStore().set('foo', 'bar'); + process.nextTick(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('foo'), 'bar'); + asyncLocalStorage.disable(); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + process.nextTick(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + asyncLocalStorage.runSyncAndReturn(() => { + assert.notStrictEqual(asyncLocalStorage.getStore(), undefined); + }); + }); + }); +}); diff --git a/test/async-hooks/test-async-local-storage-errors-async.js b/test/async-hooks/test-async-local-storage-errors-async.js new file mode 100644 index 00000000000000..c782b383e9ca95 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-errors-async.js @@ -0,0 +1,26 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +// case 1 fully async APIS (safe) +const asyncLocalStorage = new AsyncLocalStorage(); + +let i = 0; +process.setUncaughtExceptionCaptureCallback((err) => { + ++i; + assert.strictEqual(err.message, 'err' + i); + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'node'); +}); + +asyncLocalStorage.run(() => { + const store = asyncLocalStorage.getStore(); + store.set('hello', 'node'); + setTimeout(() => { + process.nextTick(() => { + assert.strictEqual(i, 2); + }); + throw new Error('err2'); + }, 0); + throw new Error('err1'); +}); diff --git a/test/async-hooks/test-async-local-storage-errors-sync-ret.js b/test/async-hooks/test-async-local-storage-errors-sync-ret.js new file mode 100644 index 00000000000000..f112df2b99dff7 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-errors-sync-ret.js @@ -0,0 +1,31 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +// case 2 using *AndReturn calls (dual behaviors) +const asyncLocalStorage = new AsyncLocalStorage(); + +let i = 0; +process.setUncaughtExceptionCaptureCallback((err) => { + ++i; + assert.strictEqual(err.message, 'err2'); + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'node'); +}); + +try { + asyncLocalStorage.runSyncAndReturn(() => { + const store = asyncLocalStorage.getStore(); + store.set('hello', 'node'); + setTimeout(() => { + process.nextTick(() => { + assert.strictEqual(i, 1); + }); + throw new Error('err2'); + }, 0); + throw new Error('err1'); + }); +} catch (e) { + assert.strictEqual(e.message, 'err1'); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +} diff --git a/test/async-hooks/test-async-local-storage-http.js b/test/async-hooks/test-async-local-storage-http.js new file mode 100644 index 00000000000000..9f107148402ec5 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-http.js @@ -0,0 +1,21 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); +const http = require('http'); + +const asyncLocalStorage = new AsyncLocalStorage(); +const server = http.createServer((req, res) => { + res.end('ok'); +}); + +server.listen(0, () => { + asyncLocalStorage.run(() => { + const store = asyncLocalStorage.getStore(); + store.set('hello', 'world'); + http.get({ host: 'localhost', port: server.address().port }, () => { + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'world'); + server.close(); + }); + }); +}); diff --git a/test/async-hooks/test-async-local-storage-nested.js b/test/async-hooks/test-async-local-storage-nested.js new file mode 100644 index 00000000000000..38330fff607ce2 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-nested.js @@ -0,0 +1,22 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +setTimeout(() => { + asyncLocalStorage.run(() => { + const asyncLocalStorage2 = new AsyncLocalStorage(); + asyncLocalStorage2.run(() => { + const store = asyncLocalStorage.getStore(); + const store2 = asyncLocalStorage2.getStore(); + store.set('hello', 'world'); + store2.set('hello', 'foo'); + setTimeout(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'world'); + assert.strictEqual(asyncLocalStorage2.getStore().get('hello'), 'foo'); + }, 200); + }); + }); +}, 100); diff --git a/test/async-hooks/test-async-local-storage-no-mix-contexts.js b/test/async-hooks/test-async-local-storage-no-mix-contexts.js new file mode 100644 index 00000000000000..561df546d4aa45 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-no-mix-contexts.js @@ -0,0 +1,38 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); +const asyncLocalStorage2 = new AsyncLocalStorage(); + +setTimeout(() => { + asyncLocalStorage.run(() => { + asyncLocalStorage2.run(() => { + const store = asyncLocalStorage.getStore(); + const store2 = asyncLocalStorage2.getStore(); + store.set('hello', 'world'); + store2.set('hello', 'foo'); + setTimeout(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'world'); + assert.strictEqual(asyncLocalStorage2.getStore().get('hello'), 'foo'); + asyncLocalStorage.exit(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + assert.strictEqual(asyncLocalStorage2.getStore().get('hello'), 'foo'); + }); + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'world'); + assert.strictEqual(asyncLocalStorage2.getStore().get('hello'), 'foo'); + }, 200); + }); + }); +}, 100); + +setTimeout(() => { + asyncLocalStorage.run(() => { + const store = asyncLocalStorage.getStore(); + store.set('hello', 'earth'); + setTimeout(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'earth'); + }, 100); + }); +}, 100); diff --git a/test/async-hooks/test-async-local-storage-promises.js b/test/async-hooks/test-async-local-storage-promises.js new file mode 100644 index 00000000000000..3b05d0f1981a3c --- /dev/null +++ b/test/async-hooks/test-async-local-storage-promises.js @@ -0,0 +1,28 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +async function main() { + const asyncLocalStorage = new AsyncLocalStorage(); + const err = new Error(); + const next = () => Promise.resolve() + .then(() => { + assert.strictEqual(asyncLocalStorage.getStore().get('a'), 1); + throw err; + }); + await new Promise((resolve, reject) => { + asyncLocalStorage.run(() => { + const store = asyncLocalStorage.getStore(); + store.set('a', 1); + next().then(resolve, reject); + }); + }) + .catch((e) => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + assert.strictEqual(e, err); + }); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); +} + +main(); From bedb27eec699713b8354cb256ad9ec38a4277a9c Mon Sep 17 00:00:00 2001 From: Andrey Pechkurov Date: Mon, 24 Feb 2020 13:00:59 +0300 Subject: [PATCH 06/15] async_hooks: add store arg in AsyncLocalStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces store as the first argument in AsyncLocalStorage's run methods. The change is motivated by the following expectation: most users are going to use a custom object as the store and an extra Map created by the previous implementation is an overhead for their use case. Important note. This is a backwards incompatible change. It was discussed and agreed an incompatible change is ok since the API is still experimental and the modified methods were only added within the last week so usage will be minimal to none. PR-URL: /~https://github.com/nodejs/node/pull/31930 Reviewed-By: Stephen Belanger Reviewed-By: Vladimir de Turckheim Reviewed-By: Matteo Collina Reviewed-By: Michaël Zasso Reviewed-By: Michael Dawson --- .../async_hooks/async-resource-vs-destroy.js | 6 +-- doc/api/async_hooks.md | 46 ++++++++++--------- lib/async_hooks.js | 14 +++--- .../test-async-local-storage-args.js | 4 +- .../test-async-local-storage-async-await.js | 2 +- ...est-async-local-storage-async-functions.js | 2 +- ...test-async-local-storage-enable-disable.js | 4 +- .../test-async-local-storage-errors-async.js | 2 +- ...est-async-local-storage-errors-sync-ret.js | 2 +- .../test-async-local-storage-http.js | 2 +- .../test-async-local-storage-misc-stores.js | 24 ++++++++++ .../test-async-local-storage-nested.js | 4 +- ...est-async-local-storage-no-mix-contexts.js | 6 +-- .../test-async-local-storage-promises.js | 2 +- 14 files changed, 73 insertions(+), 47 deletions(-) create mode 100644 test/async-hooks/test-async-local-storage-misc-stores.js diff --git a/benchmark/async_hooks/async-resource-vs-destroy.js b/benchmark/async_hooks/async-resource-vs-destroy.js index 84e17ed56d8c61..c9b9a81c5b7c7f 100644 --- a/benchmark/async_hooks/async-resource-vs-destroy.js +++ b/benchmark/async_hooks/async-resource-vs-destroy.js @@ -106,7 +106,7 @@ function buildDestroy(getServe) { function buildAsyncLocalStorage(getServe) { const asyncLocalStorage = new AsyncLocalStorage(); const server = createServer((req, res) => { - asyncLocalStorage.runSyncAndReturn(() => { + asyncLocalStorage.runSyncAndReturn({}, () => { getServe(getCLS, setCLS)(req, res); }); }); @@ -118,12 +118,12 @@ function buildAsyncLocalStorage(getServe) { function getCLS() { const store = asyncLocalStorage.getStore(); - return store.get('store'); + return store.state; } function setCLS(state) { const store = asyncLocalStorage.getStore(); - store.set('store', state); + store.state = state; } function close() { diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 08f7c46d1f4dba..454c51a13ce6ca 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -893,7 +893,7 @@ function log(...args) { } http.createServer((request, response) => { - asyncLocalStorage.run(() => { + asyncLocalStorage.run(new Map(), () => { const store = asyncLocalStorage.getStore(); store.set(kReq, request); someAsyncOperation((err, result) => { @@ -943,27 +943,27 @@ in the current process. added: REPLACEME --> -* Returns: {Map} +* Returns: {any} This method returns the current store. If this method is called outside of an asynchronous context initialized by calling `asyncLocalStorage.run` or `asyncLocalStorage.runAndReturn`, it will return `undefined`. -### `asyncLocalStorage.run(callback[, ...args])` +### `asyncLocalStorage.run(store, callback[, ...args])` +* `store` {any} * `callback` {Function} * `...args` {any} Calling `asyncLocalStorage.run(callback)` will create a new asynchronous -context. -Within the callback function and the asynchronous operations from the callback, -`asyncLocalStorage.getStore()` will return an instance of `Map` known as -"the store". This store will be persistent through the following -asynchronous calls. +context. Within the callback function and the asynchronous operations from +the callback, `asyncLocalStorage.getStore()` will return the object or +the primitive value passed into the `store` argument (known as "the store"). +This store will be persistent through the following asynchronous calls. The callback will be ran asynchronously. Optionally, arguments can be passed to the function. They will be passed to the callback function. @@ -975,10 +975,11 @@ Also, the stacktrace will be impacted by the asynchronous call. Example: ```js -asyncLocalStorage.run(() => { - asyncLocalStorage.getStore(); // Returns a Map +const store = { id: 1 }; +asyncLocalStorage.run(store, () => { + asyncLocalStorage.getStore(); // Returns the store object someAsyncOperation(() => { - asyncLocalStorage.getStore(); // Returns the same Map + asyncLocalStorage.getStore(); // Returns the same object }); }); asyncLocalStorage.getStore(); // Returns undefined @@ -1007,20 +1008,21 @@ Also, the stacktrace will be impacted by the asynchronous call. Example: ```js -asyncLocalStorage.run(() => { - asyncLocalStorage.getStore(); // Returns a Map +asyncLocalStorage.run('store value', () => { + asyncLocalStorage.getStore(); // Returns 'store value' asyncLocalStorage.exit(() => { asyncLocalStorage.getStore(); // Returns undefined }); - asyncLocalStorage.getStore(); // Returns the same Map + asyncLocalStorage.getStore(); // Returns 'store value' }); ``` -### `asyncLocalStorage.runSyncAndReturn(callback[, ...args])` +### `asyncLocalStorage.runSyncAndReturn(store, callback[, ...args])` +* `store` {any} * `callback` {Function} * `...args` {any} @@ -1038,9 +1040,10 @@ the context will be exited. Example: ```js +const store = { id: 2 }; try { - asyncLocalStorage.runSyncAndReturn(() => { - asyncLocalStorage.getStore(); // Returns a Map + asyncLocalStorage.runSyncAndReturn(store, () => { + asyncLocalStorage.getStore(); // Returns the store object throw new Error(); }); } catch (e) { @@ -1073,13 +1076,13 @@ Example: ```js // Within a call to run or runSyncAndReturn try { - asyncLocalStorage.getStore(); // Returns a Map + asyncLocalStorage.getStore(); // Returns the store object or value asyncLocalStorage.exitSyncAndReturn(() => { asyncLocalStorage.getStore(); // Returns undefined throw new Error(); }); } catch (e) { - asyncLocalStorage.getStore(); // Returns the same Map + asyncLocalStorage.getStore(); // Returns the same object or value // The error will be caught here } ``` @@ -1105,8 +1108,9 @@ It cannot be promisified using `util.promisify`. If needed, the `Promise` constructor can be used: ```js +const store = new Map(); // initialize the store new Promise((resolve, reject) => { - asyncLocalStorage.run(() => { + asyncLocalStorage.run(store, () => { someFunction((err, result) => { if (err) { return reject(err); @@ -1135,7 +1139,7 @@ the following pattern should be used: ```js async function fn() { - await asyncLocalStorage.runSyncAndReturn(() => { + await asyncLocalStorage.runSyncAndReturn(new Map(), () => { asyncLocalStorage.getStore().set('key', value); return foo(); // The return value of foo will be awaited }); diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 23f8ddde671e30..3797baf183250a 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -1,11 +1,9 @@ 'use strict'; const { - Map, NumberIsSafeInteger, ReflectApply, Symbol, - } = primordials; const { @@ -247,14 +245,14 @@ class AsyncLocalStorage { } } - _enter() { + _enter(store) { if (!this.enabled) { this.enabled = true; storageList.push(this); storageHook.enable(); } const resource = executionAsyncResource(); - resource[this.kResourceStore] = new Map(); + resource[this.kResourceStore] = store; } _exit() { @@ -264,8 +262,8 @@ class AsyncLocalStorage { } } - runSyncAndReturn(callback, ...args) { - this._enter(); + runSyncAndReturn(store, callback, ...args) { + this._enter(store); try { return callback(...args); } finally { @@ -289,8 +287,8 @@ class AsyncLocalStorage { } } - run(callback, ...args) { - this._enter(); + run(store, callback, ...args) { + this._enter(store); process.nextTick(callback, ...args); this._exit(); } diff --git a/test/async-hooks/test-async-local-storage-args.js b/test/async-hooks/test-async-local-storage-args.js index 91a3385e6eeb16..04316dff59d71a 100644 --- a/test/async-hooks/test-async-local-storage-args.js +++ b/test/async-hooks/test-async-local-storage-args.js @@ -5,14 +5,14 @@ const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); -asyncLocalStorage.run((runArg) => { +asyncLocalStorage.run({}, (runArg) => { assert.strictEqual(runArg, 1); asyncLocalStorage.exit((exitArg) => { assert.strictEqual(exitArg, 2); }, 2); }, 1); -asyncLocalStorage.runSyncAndReturn((runArg) => { +asyncLocalStorage.runSyncAndReturn({}, (runArg) => { assert.strictEqual(runArg, 'foo'); asyncLocalStorage.exitSyncAndReturn((exitArg) => { assert.strictEqual(exitArg, 'bar'); diff --git a/test/async-hooks/test-async-local-storage-async-await.js b/test/async-hooks/test-async-local-storage-async-await.js index 28c8488da62c53..a03f803186bdab 100644 --- a/test/async-hooks/test-async-local-storage-async-await.js +++ b/test/async-hooks/test-async-local-storage-async-await.js @@ -12,7 +12,7 @@ async function test() { } async function main() { - await asyncLocalStorage.runSyncAndReturn(test); + await asyncLocalStorage.runSyncAndReturn(new Map(), test); assert.strictEqual(asyncLocalStorage.getStore(), undefined); } diff --git a/test/async-hooks/test-async-local-storage-async-functions.js b/test/async-hooks/test-async-local-storage-async-functions.js index 89ac0be62c7488..a0852bc1098a1a 100644 --- a/test/async-hooks/test-async-local-storage-async-functions.js +++ b/test/async-hooks/test-async-local-storage-async-functions.js @@ -19,7 +19,7 @@ async function testAwait() { await asyncLocalStorage.exitSyncAndReturn(testOut); } -asyncLocalStorage.run(() => { +asyncLocalStorage.run(new Map(), () => { const store = asyncLocalStorage.getStore(); store.set('key', 'value'); testAwait(); // should not reject diff --git a/test/async-hooks/test-async-local-storage-enable-disable.js b/test/async-hooks/test-async-local-storage-enable-disable.js index c30d72eb805d5d..bbba8cde58d7e8 100644 --- a/test/async-hooks/test-async-local-storage-enable-disable.js +++ b/test/async-hooks/test-async-local-storage-enable-disable.js @@ -5,7 +5,7 @@ const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); -asyncLocalStorage.runSyncAndReturn(() => { +asyncLocalStorage.runSyncAndReturn(new Map(), () => { asyncLocalStorage.getStore().set('foo', 'bar'); process.nextTick(() => { assert.strictEqual(asyncLocalStorage.getStore().get('foo'), 'bar'); @@ -13,7 +13,7 @@ asyncLocalStorage.runSyncAndReturn(() => { assert.strictEqual(asyncLocalStorage.getStore(), undefined); process.nextTick(() => { assert.strictEqual(asyncLocalStorage.getStore(), undefined); - asyncLocalStorage.runSyncAndReturn(() => { + asyncLocalStorage.runSyncAndReturn(new Map(), () => { assert.notStrictEqual(asyncLocalStorage.getStore(), undefined); }); }); diff --git a/test/async-hooks/test-async-local-storage-errors-async.js b/test/async-hooks/test-async-local-storage-errors-async.js index c782b383e9ca95..b6f0b4fa742f61 100644 --- a/test/async-hooks/test-async-local-storage-errors-async.js +++ b/test/async-hooks/test-async-local-storage-errors-async.js @@ -13,7 +13,7 @@ process.setUncaughtExceptionCaptureCallback((err) => { assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'node'); }); -asyncLocalStorage.run(() => { +asyncLocalStorage.run(new Map(), () => { const store = asyncLocalStorage.getStore(); store.set('hello', 'node'); setTimeout(() => { diff --git a/test/async-hooks/test-async-local-storage-errors-sync-ret.js b/test/async-hooks/test-async-local-storage-errors-sync-ret.js index f112df2b99dff7..3b5c57a73472f6 100644 --- a/test/async-hooks/test-async-local-storage-errors-sync-ret.js +++ b/test/async-hooks/test-async-local-storage-errors-sync-ret.js @@ -14,7 +14,7 @@ process.setUncaughtExceptionCaptureCallback((err) => { }); try { - asyncLocalStorage.runSyncAndReturn(() => { + asyncLocalStorage.runSyncAndReturn(new Map(), () => { const store = asyncLocalStorage.getStore(); store.set('hello', 'node'); setTimeout(() => { diff --git a/test/async-hooks/test-async-local-storage-http.js b/test/async-hooks/test-async-local-storage-http.js index 9f107148402ec5..c7514d8280df35 100644 --- a/test/async-hooks/test-async-local-storage-http.js +++ b/test/async-hooks/test-async-local-storage-http.js @@ -10,7 +10,7 @@ const server = http.createServer((req, res) => { }); server.listen(0, () => { - asyncLocalStorage.run(() => { + asyncLocalStorage.run(new Map(), () => { const store = asyncLocalStorage.getStore(); store.set('hello', 'world'); http.get({ host: 'localhost', port: server.address().port }, () => { diff --git a/test/async-hooks/test-async-local-storage-misc-stores.js b/test/async-hooks/test-async-local-storage-misc-stores.js new file mode 100644 index 00000000000000..56873008dd644f --- /dev/null +++ b/test/async-hooks/test-async-local-storage-misc-stores.js @@ -0,0 +1,24 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +asyncLocalStorage.run(42, () => { + assert.strictEqual(asyncLocalStorage.getStore(), 42); +}); + +const runStore = { foo: 'bar' }; +asyncLocalStorage.run(runStore, () => { + assert.strictEqual(asyncLocalStorage.getStore(), runStore); +}); + +asyncLocalStorage.runSyncAndReturn('hello node', () => { + assert.strictEqual(asyncLocalStorage.getStore(), 'hello node'); +}); + +const runSyncStore = { hello: 'node' }; +asyncLocalStorage.runSyncAndReturn(runSyncStore, () => { + assert.strictEqual(asyncLocalStorage.getStore(), runSyncStore); +}); diff --git a/test/async-hooks/test-async-local-storage-nested.js b/test/async-hooks/test-async-local-storage-nested.js index 38330fff607ce2..1409a8ebc82a04 100644 --- a/test/async-hooks/test-async-local-storage-nested.js +++ b/test/async-hooks/test-async-local-storage-nested.js @@ -6,9 +6,9 @@ const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); setTimeout(() => { - asyncLocalStorage.run(() => { + asyncLocalStorage.run(new Map(), () => { const asyncLocalStorage2 = new AsyncLocalStorage(); - asyncLocalStorage2.run(() => { + asyncLocalStorage2.run(new Map(), () => { const store = asyncLocalStorage.getStore(); const store2 = asyncLocalStorage2.getStore(); store.set('hello', 'world'); diff --git a/test/async-hooks/test-async-local-storage-no-mix-contexts.js b/test/async-hooks/test-async-local-storage-no-mix-contexts.js index 561df546d4aa45..3a6b352c94ceee 100644 --- a/test/async-hooks/test-async-local-storage-no-mix-contexts.js +++ b/test/async-hooks/test-async-local-storage-no-mix-contexts.js @@ -7,8 +7,8 @@ const asyncLocalStorage = new AsyncLocalStorage(); const asyncLocalStorage2 = new AsyncLocalStorage(); setTimeout(() => { - asyncLocalStorage.run(() => { - asyncLocalStorage2.run(() => { + asyncLocalStorage.run(new Map(), () => { + asyncLocalStorage2.run(new Map(), () => { const store = asyncLocalStorage.getStore(); const store2 = asyncLocalStorage2.getStore(); store.set('hello', 'world'); @@ -28,7 +28,7 @@ setTimeout(() => { }, 100); setTimeout(() => { - asyncLocalStorage.run(() => { + asyncLocalStorage.run(new Map(), () => { const store = asyncLocalStorage.getStore(); store.set('hello', 'earth'); setTimeout(() => { diff --git a/test/async-hooks/test-async-local-storage-promises.js b/test/async-hooks/test-async-local-storage-promises.js index 3b05d0f1981a3c..0e4968534bc3e2 100644 --- a/test/async-hooks/test-async-local-storage-promises.js +++ b/test/async-hooks/test-async-local-storage-promises.js @@ -12,7 +12,7 @@ async function main() { throw err; }); await new Promise((resolve, reject) => { - asyncLocalStorage.run(() => { + asyncLocalStorage.run(new Map(), () => { const store = asyncLocalStorage.getStore(); store.set('a', 1); next().then(resolve, reject); From be305fe942285314750bd99f22260c42f1c6819a Mon Sep 17 00:00:00 2001 From: Andrey Pechkurov Date: Fri, 28 Feb 2020 10:07:47 +0300 Subject: [PATCH 07/15] test: improve disable AsyncLocalStorage test PR-URL: /~https://github.com/nodejs/node/pull/31998 Reviewed-By: Anna Henningsen Reviewed-By: Vladimir de Turckheim Reviewed-By: James M Snell --- test/async-hooks/test-async-local-storage-enable-disable.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/async-hooks/test-async-local-storage-enable-disable.js b/test/async-hooks/test-async-local-storage-enable-disable.js index bbba8cde58d7e8..22a3f5f6c8c43f 100644 --- a/test/async-hooks/test-async-local-storage-enable-disable.js +++ b/test/async-hooks/test-async-local-storage-enable-disable.js @@ -9,6 +9,9 @@ asyncLocalStorage.runSyncAndReturn(new Map(), () => { asyncLocalStorage.getStore().set('foo', 'bar'); process.nextTick(() => { assert.strictEqual(asyncLocalStorage.getStore().get('foo'), 'bar'); + process.nextTick(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + }); asyncLocalStorage.disable(); assert.strictEqual(asyncLocalStorage.getStore(), undefined); process.nextTick(() => { From ecc59354f0257e2979f8b0cf3132ad010a1842f9 Mon Sep 17 00:00:00 2001 From: Andrey Pechkurov Date: Wed, 26 Feb 2020 09:38:48 +0300 Subject: [PATCH 08/15] test: add GC test for disabled AsyncLocalStorage PR-URL: /~https://github.com/nodejs/node/pull/31995 Reviewed-By: Anna Henningsen Reviewed-By: Gireesh Punathil Reviewed-By: Stephen Belanger Reviewed-By: Vladimir de Turckheim Reviewed-By: James M Snell Reviewed-By: Michael Dawson --- .../test-async-local-storage-gcable.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 test/async-hooks/test-async-local-storage-gcable.js diff --git a/test/async-hooks/test-async-local-storage-gcable.js b/test/async-hooks/test-async-local-storage-gcable.js new file mode 100644 index 00000000000000..37b04b38d14588 --- /dev/null +++ b/test/async-hooks/test-async-local-storage-gcable.js @@ -0,0 +1,20 @@ +'use strict'; +// Flags: --expose_gc + +// This test ensures that AsyncLocalStorage gets gced once it was disabled +// and no strong references remain in userland. + +const common = require('../common'); +const { AsyncLocalStorage } = require('async_hooks'); +const onGC = require('../common/ongc'); + +let asyncLocalStorage = new AsyncLocalStorage(); + +asyncLocalStorage.runSyncAndReturn({}, () => { + asyncLocalStorage.disable(); + + onGC(asyncLocalStorage, { ongc: common.mustCall() }); +}); + +asyncLocalStorage = null; +global.gc(); From 50361176d1c797afe4a8d5f7019c71540c086880 Mon Sep 17 00:00:00 2001 From: Andrey Pechkurov Date: Wed, 4 Mar 2020 14:57:56 +0300 Subject: [PATCH 09/15] async_hooks: fix ctx loss after nested ALS calls PR-URL: /~https://github.com/nodejs/node/pull/32085 Reviewed-By: Stephen Belanger Reviewed-By: Vladimir de Turckheim Reviewed-By: Michael Dawson --- lib/async_hooks.js | 21 +++++---- ...test-async-local-storage-enable-disable.js | 8 ++++ .../test-async-local-storage-nested.js | 44 +++++++++++++------ 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 3797baf183250a..bd3cd57d022f4b 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -255,23 +255,21 @@ class AsyncLocalStorage { resource[this.kResourceStore] = store; } - _exit() { - const resource = executionAsyncResource(); - if (resource) { - resource[this.kResourceStore] = undefined; - } - } - runSyncAndReturn(store, callback, ...args) { + const resource = executionAsyncResource(); + const outerStore = resource[this.kResourceStore]; this._enter(store); try { return callback(...args); } finally { - this._exit(); + resource[this.kResourceStore] = outerStore; } } exitSyncAndReturn(callback, ...args) { + if (!this.enabled) { + return callback(...args); + } this.enabled = false; try { return callback(...args); @@ -288,12 +286,17 @@ class AsyncLocalStorage { } run(store, callback, ...args) { + const resource = executionAsyncResource(); + const outerStore = resource[this.kResourceStore]; this._enter(store); process.nextTick(callback, ...args); - this._exit(); + resource[this.kResourceStore] = outerStore; } exit(callback, ...args) { + if (!this.enabled) { + return process.nextTick(callback, ...args); + } this.enabled = false; process.nextTick(callback, ...args); this.enabled = true; diff --git a/test/async-hooks/test-async-local-storage-enable-disable.js b/test/async-hooks/test-async-local-storage-enable-disable.js index 22a3f5f6c8c43f..93132079827eeb 100644 --- a/test/async-hooks/test-async-local-storage-enable-disable.js +++ b/test/async-hooks/test-async-local-storage-enable-disable.js @@ -12,8 +12,16 @@ asyncLocalStorage.runSyncAndReturn(new Map(), () => { process.nextTick(() => { assert.strictEqual(asyncLocalStorage.getStore(), undefined); }); + asyncLocalStorage.disable(); assert.strictEqual(asyncLocalStorage.getStore(), undefined); + + // Calls to exit() should not mess with enabled status + asyncLocalStorage.exit(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + }); + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + process.nextTick(() => { assert.strictEqual(asyncLocalStorage.getStore(), undefined); asyncLocalStorage.runSyncAndReturn(new Map(), () => { diff --git a/test/async-hooks/test-async-local-storage-nested.js b/test/async-hooks/test-async-local-storage-nested.js index 1409a8ebc82a04..143d5d45de9e25 100644 --- a/test/async-hooks/test-async-local-storage-nested.js +++ b/test/async-hooks/test-async-local-storage-nested.js @@ -4,19 +4,35 @@ const assert = require('assert'); const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); +const outer = {}; +const inner = {}; -setTimeout(() => { - asyncLocalStorage.run(new Map(), () => { - const asyncLocalStorage2 = new AsyncLocalStorage(); - asyncLocalStorage2.run(new Map(), () => { - const store = asyncLocalStorage.getStore(); - const store2 = asyncLocalStorage2.getStore(); - store.set('hello', 'world'); - store2.set('hello', 'foo'); - setTimeout(() => { - assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'world'); - assert.strictEqual(asyncLocalStorage2.getStore().get('hello'), 'foo'); - }, 200); - }); +function testInner() { + assert.strictEqual(asyncLocalStorage.getStore(), outer); + + asyncLocalStorage.run(inner, () => { + assert.strictEqual(asyncLocalStorage.getStore(), inner); + }); + assert.strictEqual(asyncLocalStorage.getStore(), outer); + + asyncLocalStorage.exit(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + }); + assert.strictEqual(asyncLocalStorage.getStore(), outer); + + asyncLocalStorage.runSyncAndReturn(inner, () => { + assert.strictEqual(asyncLocalStorage.getStore(), inner); }); -}, 100); + assert.strictEqual(asyncLocalStorage.getStore(), outer); + + asyncLocalStorage.exitSyncAndReturn(() => { + assert.strictEqual(asyncLocalStorage.getStore(), undefined); + }); + assert.strictEqual(asyncLocalStorage.getStore(), outer); +} + +asyncLocalStorage.run(outer, testInner); +assert.strictEqual(asyncLocalStorage.getStore(), undefined); + +asyncLocalStorage.runSyncAndReturn(outer, testInner); +assert.strictEqual(asyncLocalStorage.getStore(), undefined); From e9b391823cf86ea9cf014c7aa10a65eafc86b432 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Tue, 25 Feb 2020 13:46:46 +0800 Subject: [PATCH 10/15] async_hooks: add sync enterWith to ALS This allows transitioning the entire following sync and async execution sub-tree to the given async storage context. With this one can be sure the context binding will remain for any following sync activity and all descending async execution whereas the `run*(...)` methods must wrap everything that is intended to exist within the context. This is helpful for scenarios such as prepending a `'connection'` event to an http server which binds everything that occurs within each request to the given context. This is helpful for APMs to minimize the need for patching and especially adding closures. PR-URL: /~https://github.com/nodejs/node/pull/31945 Reviewed-By: Vladimir de Turckheim Reviewed-By: Matteo Collina Reviewed-By: Michael Dawson --- doc/api/async_hooks.md | 42 +++++++++++++++++++ lib/async_hooks.js | 6 +-- .../test-async-local-storage-enter-with.js | 20 +++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 test/async-hooks/test-async-local-storage-enter-with.js diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 454c51a13ce6ca..329f1a025e0334 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -950,6 +950,48 @@ If this method is called outside of an asynchronous context initialized by calling `asyncLocalStorage.run` or `asyncLocalStorage.runAndReturn`, it will return `undefined`. +### `asyncLocalStorage.enterWith(store)` + + +* `store` {any} + +Calling `asyncLocalStorage.enterWith(store)` will transition into the context +for the remainder of the current synchronous execution and will persist +through any following asynchronous calls. + +Example: + +```js +const store = { id: 1 }; +asyncLocalStorage.enterWith(store); +asyncLocalStorage.getStore(); // Returns the store object +someAsyncOperation(() => { + asyncLocalStorage.getStore(); // Returns the same object +}); +``` + +This transition will continue for the _entire_ synchronous execution. +This means that if, for example, the context is entered within an event +handler subsequent event handlers will also run within that context unless +specifically bound to another context with an `AsyncResource`. + +```js +const store = { id: 1 }; + +emitter.on('my-event', () => { + asyncLocalStorage.enterWith(store); +}); +emitter.on('my-event', () => { + asyncLocalStorage.getStore(); // Returns the same object +}); + +asyncLocalStorage.getStore(); // Returns undefined +emitter.emit('my-event'); +asyncLocalStorage.getStore(); // Returns the same object +``` + ### `asyncLocalStorage.run(store, callback[, ...args])` Creates a new instance of `AsyncLocalStorage`. Store is only provided within a -`run` or a `runSyncAndReturn` method call. +`run` method call. ### `asyncLocalStorage.disable()` - -* `callback` {Function} -* `...args` {any} - -Calling `asyncLocalStorage.exit(callback)` will create a new asynchronous -context. -Within the callback function and the asynchronous operations from the callback, -`asyncLocalStorage.getStore()` will return `undefined`. - -The callback will be ran asynchronously. Optionally, arguments can be passed -to the function. They will be passed to the callback function. - -If an error is thrown by the callback function, it will not be caught by -a `try/catch` block as the callback is ran in a new asynchronous resource. -Also, the stacktrace will be impacted by the asynchronous call. - -Example: - -```js -asyncLocalStorage.run('store value', () => { - asyncLocalStorage.getStore(); // Returns 'store value' - asyncLocalStorage.exit(() => { - asyncLocalStorage.getStore(); // Returns undefined - }); - asyncLocalStorage.getStore(); // Returns 'store value' -}); -``` - -### `asyncLocalStorage.runSyncAndReturn(store, callback[, ...args])` - - -* `store` {any} -* `callback` {Function} -* `...args` {any} - This methods runs a function synchronously within a context and return its return value. The store is not accessible outside of the callback function or the asynchronous operations created within the callback. @@ -1075,16 +1006,16 @@ the asynchronous operations created within the callback. Optionally, arguments can be passed to the function. They will be passed to the callback function. -If the callback function throws an error, it will be thrown by -`runSyncAndReturn` too. The stacktrace will not be impacted by this call and -the context will be exited. +If the callback function throws an error, it will be thrown by `run` too. +The stacktrace will not be impacted by this call and the context will +be exited. Example: ```js const store = { id: 2 }; try { - asyncLocalStorage.runSyncAndReturn(store, () => { + asyncLocalStorage.run(store, () => { asyncLocalStorage.getStore(); // Returns the store object throw new Error(); }); @@ -1094,7 +1025,7 @@ try { } ``` -### `asyncLocalStorage.exitSyncAndReturn(callback[, ...args])` +### `asyncLocalStorage.exit(callback[, ...args])` @@ -1109,17 +1040,17 @@ the asynchronous operations created within the callback. Optionally, arguments can be passed to the function. They will be passed to the callback function. -If the callback function throws an error, it will be thrown by -`exitSyncAndReturn` too. The stacktrace will not be impacted by this call and +If the callback function throws an error, it will be thrown by `exit` too. +The stacktrace will not be impacted by this call and the context will be re-entered. Example: ```js -// Within a call to run or runSyncAndReturn +// Within a call to run try { asyncLocalStorage.getStore(); // Returns the store object or value - asyncLocalStorage.exitSyncAndReturn(() => { + asyncLocalStorage.exit(() => { asyncLocalStorage.getStore(); // Returns undefined throw new Error(); }); @@ -1129,59 +1060,14 @@ try { } ``` -### Choosing between `run` and `runSyncAndReturn` - -#### When to choose `run` - -`run` is asynchronous. It is called with a callback function that -runs within a new asynchronous call. This is the most explicit behavior as -everything that is executed within the callback of `run` (including further -asynchronous operations) will have access to the store. - -If an instance of `AsyncLocalStorage` is used for error management (for -instance, with `process.setUncaughtExceptionCaptureCallback`), only -exceptions thrown in the scope of the callback function will be associated -with the context. - -This method is the safest as it provides strong scoping and consistent -behavior. - -It cannot be promisified using `util.promisify`. If needed, the `Promise` -constructor can be used: - -```js -const store = new Map(); // initialize the store -new Promise((resolve, reject) => { - asyncLocalStorage.run(store, () => { - someFunction((err, result) => { - if (err) { - return reject(err); - } - return resolve(result); - }); - }); -}); -``` - -#### When to choose `runSyncAndReturn` - -`runSyncAndReturn` is synchronous. The callback function will be executed -synchronously and its return value will be returned by `runSyncAndReturn`. -The store will only be accessible from within the callback -function and the asynchronous operations created within this scope. -If the callback throws an error, `runSyncAndReturn` will throw it and it will -not be associated with the context. - -This method provides good scoping while being synchronous. - -#### Usage with `async/await` +### Usage with `async/await` If, within an async function, only one `await` call is to run within a context, the following pattern should be used: ```js async function fn() { - await asyncLocalStorage.runSyncAndReturn(new Map(), () => { + await asyncLocalStorage.run(new Map(), () => { asyncLocalStorage.getStore().set('key', value); return foo(); // The return value of foo will be awaited }); @@ -1189,8 +1075,8 @@ async function fn() { ``` In this example, the store is only available in the callback function and the -functions called by `foo`. Outside of `runSyncAndReturn`, calling `getStore` -will return `undefined`. +functions called by `foo`. Outside of `run`, calling `getStore` will return +`undefined`. [`after` callback]: #async_hooks_after_asyncid [`before` callback]: #async_hooks_before_asyncid diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 97e1e1912e55a0..1889511ca67230 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -255,7 +255,7 @@ class AsyncLocalStorage { resource[this.kResourceStore] = store; } - runSyncAndReturn(store, callback, ...args) { + run(store, callback, ...args) { const resource = new AsyncResource('AsyncLocalStorage'); return resource.runInAsyncScope(() => { this.enterWith(store); @@ -263,7 +263,7 @@ class AsyncLocalStorage { }); } - exitSyncAndReturn(callback, ...args) { + exit(callback, ...args) { if (!this.enabled) { return callback(...args); } @@ -281,22 +281,6 @@ class AsyncLocalStorage { return resource[this.kResourceStore]; } } - - run(store, callback, ...args) { - process.nextTick(() => { - this.enterWith(store); - return callback(...args); - }); - } - - exit(callback, ...args) { - if (!this.enabled) { - return process.nextTick(callback, ...args); - } - this.enabled = false; - process.nextTick(callback, ...args); - this.enabled = true; - } } // Placing all exports down here because the exported classes won't export diff --git a/test/async-hooks/test-async-local-storage-args.js b/test/async-hooks/test-async-local-storage-args.js index 04316dff59d71a..71853ecc4af0eb 100644 --- a/test/async-hooks/test-async-local-storage-args.js +++ b/test/async-hooks/test-async-local-storage-args.js @@ -6,15 +6,8 @@ const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); asyncLocalStorage.run({}, (runArg) => { - assert.strictEqual(runArg, 1); - asyncLocalStorage.exit((exitArg) => { - assert.strictEqual(exitArg, 2); - }, 2); -}, 1); - -asyncLocalStorage.runSyncAndReturn({}, (runArg) => { assert.strictEqual(runArg, 'foo'); - asyncLocalStorage.exitSyncAndReturn((exitArg) => { + asyncLocalStorage.exit((exitArg) => { assert.strictEqual(exitArg, 'bar'); }, 'bar'); }, 'foo'); diff --git a/test/async-hooks/test-async-local-storage-async-await.js b/test/async-hooks/test-async-local-storage-async-await.js index a03f803186bdab..64333eee938f3d 100644 --- a/test/async-hooks/test-async-local-storage-async-await.js +++ b/test/async-hooks/test-async-local-storage-async-await.js @@ -12,7 +12,7 @@ async function test() { } async function main() { - await asyncLocalStorage.runSyncAndReturn(new Map(), test); + await asyncLocalStorage.run(new Map(), test); assert.strictEqual(asyncLocalStorage.getStore(), undefined); } diff --git a/test/async-hooks/test-async-local-storage-async-functions.js b/test/async-hooks/test-async-local-storage-async-functions.js index a0852bc1098a1a..ea186e9f68da4e 100644 --- a/test/async-hooks/test-async-local-storage-async-functions.js +++ b/test/async-hooks/test-async-local-storage-async-functions.js @@ -16,7 +16,7 @@ async function testAwait() { await foo(); assert.notStrictEqual(asyncLocalStorage.getStore(), undefined); assert.strictEqual(asyncLocalStorage.getStore().get('key'), 'value'); - await asyncLocalStorage.exitSyncAndReturn(testOut); + await asyncLocalStorage.exit(testOut); } asyncLocalStorage.run(new Map(), () => { diff --git a/test/async-hooks/test-async-local-storage-enable-disable.js b/test/async-hooks/test-async-local-storage-enable-disable.js index 93132079827eeb..15ab2f5d9ebbb2 100644 --- a/test/async-hooks/test-async-local-storage-enable-disable.js +++ b/test/async-hooks/test-async-local-storage-enable-disable.js @@ -5,7 +5,7 @@ const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); -asyncLocalStorage.runSyncAndReturn(new Map(), () => { +asyncLocalStorage.run(new Map(), () => { asyncLocalStorage.getStore().set('foo', 'bar'); process.nextTick(() => { assert.strictEqual(asyncLocalStorage.getStore().get('foo'), 'bar'); @@ -24,7 +24,7 @@ asyncLocalStorage.runSyncAndReturn(new Map(), () => { process.nextTick(() => { assert.strictEqual(asyncLocalStorage.getStore(), undefined); - asyncLocalStorage.runSyncAndReturn(new Map(), () => { + asyncLocalStorage.run(new Map(), () => { assert.notStrictEqual(asyncLocalStorage.getStore(), undefined); }); }); diff --git a/test/async-hooks/test-async-local-storage-errors-async.js b/test/async-hooks/test-async-local-storage-errors-async.js deleted file mode 100644 index b6f0b4fa742f61..00000000000000 --- a/test/async-hooks/test-async-local-storage-errors-async.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; -require('../common'); -const assert = require('assert'); -const { AsyncLocalStorage } = require('async_hooks'); - -// case 1 fully async APIS (safe) -const asyncLocalStorage = new AsyncLocalStorage(); - -let i = 0; -process.setUncaughtExceptionCaptureCallback((err) => { - ++i; - assert.strictEqual(err.message, 'err' + i); - assert.strictEqual(asyncLocalStorage.getStore().get('hello'), 'node'); -}); - -asyncLocalStorage.run(new Map(), () => { - const store = asyncLocalStorage.getStore(); - store.set('hello', 'node'); - setTimeout(() => { - process.nextTick(() => { - assert.strictEqual(i, 2); - }); - throw new Error('err2'); - }, 0); - throw new Error('err1'); -}); diff --git a/test/async-hooks/test-async-local-storage-errors-sync-ret.js b/test/async-hooks/test-async-local-storage-errors.js similarity index 93% rename from test/async-hooks/test-async-local-storage-errors-sync-ret.js rename to test/async-hooks/test-async-local-storage-errors.js index 3b5c57a73472f6..0dd5754e02cbd9 100644 --- a/test/async-hooks/test-async-local-storage-errors-sync-ret.js +++ b/test/async-hooks/test-async-local-storage-errors.js @@ -14,7 +14,7 @@ process.setUncaughtExceptionCaptureCallback((err) => { }); try { - asyncLocalStorage.runSyncAndReturn(new Map(), () => { + asyncLocalStorage.run(new Map(), () => { const store = asyncLocalStorage.getStore(); store.set('hello', 'node'); setTimeout(() => { diff --git a/test/async-hooks/test-async-local-storage-gcable.js b/test/async-hooks/test-async-local-storage-gcable.js index 37b04b38d14588..f0d23a0d22793b 100644 --- a/test/async-hooks/test-async-local-storage-gcable.js +++ b/test/async-hooks/test-async-local-storage-gcable.js @@ -10,7 +10,7 @@ const onGC = require('../common/ongc'); let asyncLocalStorage = new AsyncLocalStorage(); -asyncLocalStorage.runSyncAndReturn({}, () => { +asyncLocalStorage.run({}, () => { asyncLocalStorage.disable(); onGC(asyncLocalStorage, { ongc: common.mustCall() }); diff --git a/test/async-hooks/test-async-local-storage-misc-stores.js b/test/async-hooks/test-async-local-storage-misc-stores.js index 56873008dd644f..fbbbb52a5d7a6b 100644 --- a/test/async-hooks/test-async-local-storage-misc-stores.js +++ b/test/async-hooks/test-async-local-storage-misc-stores.js @@ -5,20 +5,11 @@ const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); -asyncLocalStorage.run(42, () => { - assert.strictEqual(asyncLocalStorage.getStore(), 42); +asyncLocalStorage.run('hello node', () => { + assert.strictEqual(asyncLocalStorage.getStore(), 'hello node'); }); -const runStore = { foo: 'bar' }; +const runStore = { hello: 'node' }; asyncLocalStorage.run(runStore, () => { assert.strictEqual(asyncLocalStorage.getStore(), runStore); }); - -asyncLocalStorage.runSyncAndReturn('hello node', () => { - assert.strictEqual(asyncLocalStorage.getStore(), 'hello node'); -}); - -const runSyncStore = { hello: 'node' }; -asyncLocalStorage.runSyncAndReturn(runSyncStore, () => { - assert.strictEqual(asyncLocalStorage.getStore(), runSyncStore); -}); diff --git a/test/async-hooks/test-async-local-storage-nested.js b/test/async-hooks/test-async-local-storage-nested.js index 143d5d45de9e25..870c294b74d22a 100644 --- a/test/async-hooks/test-async-local-storage-nested.js +++ b/test/async-hooks/test-async-local-storage-nested.js @@ -19,20 +19,7 @@ function testInner() { assert.strictEqual(asyncLocalStorage.getStore(), undefined); }); assert.strictEqual(asyncLocalStorage.getStore(), outer); - - asyncLocalStorage.runSyncAndReturn(inner, () => { - assert.strictEqual(asyncLocalStorage.getStore(), inner); - }); - assert.strictEqual(asyncLocalStorage.getStore(), outer); - - asyncLocalStorage.exitSyncAndReturn(() => { - assert.strictEqual(asyncLocalStorage.getStore(), undefined); - }); - assert.strictEqual(asyncLocalStorage.getStore(), outer); } asyncLocalStorage.run(outer, testInner); assert.strictEqual(asyncLocalStorage.getStore(), undefined); - -asyncLocalStorage.runSyncAndReturn(outer, testInner); -assert.strictEqual(asyncLocalStorage.getStore(), undefined); From fce02724b1bc908b70cbd1c0c89e5f3e760079a9 Mon Sep 17 00:00:00 2001 From: Andrey Pechkurov Date: Fri, 10 Apr 2020 11:07:40 +0300 Subject: [PATCH 15/15] doc: improve AsyncLocalStorage sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: /~https://github.com/nodejs/node/pull/32757 Reviewed-By: Michaël Zasso Reviewed-By: Chengzhong Wu Reviewed-By: Colin Ihrig Reviewed-By: Gerhard Stöbich Reviewed-By: Luigi Pinca --- doc/api/async_hooks.md | 49 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index b2f130fe0438c2..12c06708816b55 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -869,41 +869,40 @@ chains. It allows storing data throughout the lifetime of a web request or any other asynchronous duration. It is similar to thread-local storage in other languages. -The following example builds a logger that will always know the current HTTP -request and uses it to display enhanced logs without needing to explicitly -provide the current HTTP request to it. +The following example uses `AsyncLocalStorage` to build a simple logger +that assigns IDs to incoming HTTP requests and includes them in messages +logged within each request. ```js -const { AsyncLocalStorage } = require('async_hooks'); const http = require('http'); +const { AsyncLocalStorage } = require('async_hooks'); -const kReq = 'CURRENT_REQUEST'; const asyncLocalStorage = new AsyncLocalStorage(); -function log(...args) { - const store = asyncLocalStorage.getStore(); - // Make sure the store exists and it contains a request. - if (store && store.has(kReq)) { - const req = store.get(kReq); - // Prints `GET /items ERR could not do something - console.log(req.method, req.url, ...args); - } else { - console.log(...args); - } +function logWithId(msg) { + const id = asyncLocalStorage.getStore(); + console.log(`${id !== undefined ? id : '-'}:`, msg); } -http.createServer((request, response) => { - asyncLocalStorage.run(new Map(), () => { - const store = asyncLocalStorage.getStore(); - store.set(kReq, request); - someAsyncOperation((err, result) => { - if (err) { - log('ERR', err.message); - } +let idSeq = 0; +http.createServer((req, res) => { + asyncLocalStorage.run(idSeq++, () => { + logWithId('start'); + // Imagine any chain of async operations here + setImmediate(() => { + logWithId('finish'); + res.end(); }); }); -}) -.listen(8080); +}).listen(8080); + +http.get('http://localhost:8080'); +http.get('http://localhost:8080'); +// Prints: +// 0: start +// 1: start +// 0: finish +// 1: finish ``` When having multiple instances of `AsyncLocalStorage`, they are independent