diff options
author | vdeturckheim <vlad2t@hotmail.com> | 2021-03-09 17:06:20 +0300 |
---|---|---|
committer | Michael Dawson <mdawson@devrus.com> | 2021-06-04 21:03:43 +0300 |
commit | 7612d82c44c3a7f7e8bf58d57541606f3e7da37b (patch) | |
tree | 6eccb774975804a27c1b6a2226f10e2ae2891f9b /doc/api/async_hooks.md | |
parent | 6fe89a1fa6c10e1b68fe5c5d388a9d3e56bec138 (diff) |
async_hooks: stabilize part of AsyncLocalStorage
Mark AsyncLocalStorage constructor,
AsyncLocalStorage.prototype.getStore(),
and AsyncLocalStorage.prototype.run as
stable.
PR-URL: https://github.com/nodejs/node/pull/37675
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Reviewed-By: Andrey Pechkurov <apechkurov@gmail.com>
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Bryan English <bryan@bryanenglish.com>
Diffstat (limited to 'doc/api/async_hooks.md')
-rw-r--r-- | doc/api/async_hooks.md | 563 |
1 files changed, 4 insertions, 559 deletions
diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 55cc0542561..f6805102fdf 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -664,575 +664,20 @@ like I/O, connection pooling, or managing callback queues may use the ### Class: `AsyncResource` -The class `AsyncResource` is designed to be extended by the embedder's async -resources. Using this, users can easily trigger the lifetime events of their -own resources. - -The `init` hook will trigger when an `AsyncResource` is instantiated. - -The following is an overview of the `AsyncResource` API. - -```js -const { AsyncResource, executionAsyncId } = require('async_hooks'); - -// AsyncResource() is meant to be extended. Instantiating a -// new AsyncResource() also triggers init. If triggerAsyncId is omitted then -// async_hook.executionAsyncId() is used. -const asyncResource = new AsyncResource( - type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false } -); - -// Run a function in the execution context of the resource. This will -// * establish the context of the resource -// * trigger the AsyncHooks before callbacks -// * call the provided function `fn` with the supplied arguments -// * trigger the AsyncHooks after callbacks -// * restore the original execution context -asyncResource.runInAsyncScope(fn, thisArg, ...args); - -// Call AsyncHooks destroy callbacks. -asyncResource.emitDestroy(); - -// Return the unique ID assigned to the AsyncResource instance. -asyncResource.asyncId(); - -// Return the trigger ID for the AsyncResource instance. -asyncResource.triggerAsyncId(); -``` - -#### `new AsyncResource(type[, options])` - -* `type` {string} The type of async event. -* `options` {Object} - * `triggerAsyncId` {number} The ID of the execution context that created this - async event. **Default:** `executionAsyncId()`. - * `requireManualDestroy` {boolean} If set to `true`, disables `emitDestroy` - when the object is garbage collected. This usually does not need to be set - (even if `emitDestroy` is called manually), unless the resource's `asyncId` - is retrieved and the sensitive API's `emitDestroy` is called with it. - When set to `false`, the `emitDestroy` call on garbage collection - will only take place if there is at least one active `destroy` hook. - **Default:** `false`. - -Example usage: - -```js -class DBQuery extends AsyncResource { - constructor(db) { - super('DBQuery'); - this.db = db; - } - - getInfo(query, callback) { - this.db.get(query, (err, data) => { - this.runInAsyncScope(callback, null, err, data); - }); - } - - close() { - this.db = null; - this.emitDestroy(); - } -} -``` - -#### Static method: `AsyncResource.bind(fn[, type, [thisArg]])` -<!-- YAML -added: - - v14.8.0 - - v12.19.0 -changes: - - version: v16.0.0 - pr-url: https://github.com/nodejs/node/pull/36782 - description: Added optional thisArg. ---> - -* `fn` {Function} The function to bind to the current execution context. -* `type` {string} An optional name to associate with the underlying - `AsyncResource`. -* `thisArg` {any} - -Binds the given function to the current execution context. - -The returned function will have an `asyncResource` property referencing -the `AsyncResource` to which the function is bound. - -#### `asyncResource.bind(fn[, thisArg])` -<!-- YAML -added: - - v14.8.0 - - v12.19.0 -changes: - - version: v16.0.0 - pr-url: https://github.com/nodejs/node/pull/36782 - description: Added optional thisArg. ---> - -* `fn` {Function} The function to bind to the current `AsyncResource`. -* `thisArg` {any} - -Binds the given function to execute to this `AsyncResource`'s scope. - -The returned function will have an `asyncResource` property referencing -the `AsyncResource` to which the function is bound. - -#### `asyncResource.runInAsyncScope(fn[, thisArg, ...args])` -<!-- YAML -added: v9.6.0 ---> - -* `fn` {Function} The function to call in the execution context of this async - resource. -* `thisArg` {any} The receiver to be used for the function call. -* `...args` {any} Optional arguments to pass to the function. - -Call the provided function with the provided arguments in the execution context -of the async resource. This will establish the context, trigger the AsyncHooks -before callbacks, call the function, trigger the AsyncHooks after callbacks, and -then restore the original execution context. - -#### `asyncResource.emitDestroy()` - -* Returns: {AsyncResource} A reference to `asyncResource`. - -Call all `destroy` hooks. This should only ever be called once. An error will -be thrown if it is called more than once. This **must** be manually called. If -the resource is left to be collected by the GC then the `destroy` hooks will -never be called. - -#### `asyncResource.asyncId()` - -* Returns: {number} The unique `asyncId` assigned to the resource. - -#### `asyncResource.triggerAsyncId()` - -* Returns: {number} The same `triggerAsyncId` that is passed to the - `AsyncResource` constructor. - -<a id="async-resource-worker-pool"></a> -### Using `AsyncResource` for a `Worker` thread pool - -The following example shows how to use the `AsyncResource` class to properly -provide async tracking for a [`Worker`][] pool. Other resource pools, such as -database connection pools, can follow a similar model. - -Assuming that the task is adding two numbers, using a file named -`task_processor.js` with the following content: - -```js -const { parentPort } = require('worker_threads'); -parentPort.on('message', (task) => { - parentPort.postMessage(task.a + task.b); -}); -``` - -a Worker pool around it could use the following structure: - -```js -const { AsyncResource } = require('async_hooks'); -const { EventEmitter } = require('events'); -const path = require('path'); -const { Worker } = require('worker_threads'); - -const kTaskInfo = Symbol('kTaskInfo'); -const kWorkerFreedEvent = Symbol('kWorkerFreedEvent'); - -class WorkerPoolTaskInfo extends AsyncResource { - constructor(callback) { - super('WorkerPoolTaskInfo'); - this.callback = callback; - } - - done(err, result) { - this.runInAsyncScope(this.callback, null, err, result); - this.emitDestroy(); // `TaskInfo`s are used only once. - } -} - -class WorkerPool extends EventEmitter { - constructor(numThreads) { - super(); - this.numThreads = numThreads; - this.workers = []; - this.freeWorkers = []; - this.tasks = []; - - for (let i = 0; i < numThreads; i++) - this.addNewWorker(); - - // Any time the kWorkerFreedEvent is emitted, dispatch - // the next task pending in the queue, if any. - this.on(kWorkerFreedEvent, () => { - if (this.tasks.length > 0) { - const { task, callback } = this.tasks.shift(); - this.runTask(task, callback); - } - }); - } - - addNewWorker() { - const worker = new Worker(path.resolve(__dirname, 'task_processor.js')); - worker.on('message', (result) => { - // In case of success: Call the callback that was passed to `runTask`, - // remove the `TaskInfo` associated with the Worker, and mark it as free - // again. - worker[kTaskInfo].done(null, result); - worker[kTaskInfo] = null; - this.freeWorkers.push(worker); - this.emit(kWorkerFreedEvent); - }); - worker.on('error', (err) => { - // In case of an uncaught exception: Call the callback that was passed to - // `runTask` with the error. - if (worker[kTaskInfo]) - worker[kTaskInfo].done(err, null); - else - this.emit('error', err); - // Remove the worker from the list and start a new Worker to replace the - // current one. - this.workers.splice(this.workers.indexOf(worker), 1); - this.addNewWorker(); - }); - this.workers.push(worker); - this.freeWorkers.push(worker); - this.emit(kWorkerFreedEvent); - } - - runTask(task, callback) { - if (this.freeWorkers.length === 0) { - // No free threads, wait until a worker thread becomes free. - this.tasks.push({ task, callback }); - return; - } - - const worker = this.freeWorkers.pop(); - worker[kTaskInfo] = new WorkerPoolTaskInfo(callback); - worker.postMessage(task); - } - - close() { - for (const worker of this.workers) worker.terminate(); - } -} - -module.exports = WorkerPool; -``` - -Without the explicit tracking added by the `WorkerPoolTaskInfo` objects, -it would appear that the callbacks are associated with the individual `Worker` -objects. However, the creation of the `Worker`s is not associated with the -creation of the tasks and does not provide information about when tasks -were scheduled. - -This pool could be used as follows: - -```js -const WorkerPool = require('./worker_pool.js'); -const os = require('os'); - -const pool = new WorkerPool(os.cpus().length); - -let finished = 0; -for (let i = 0; i < 10; i++) { - pool.runTask({ a: 42, b: 100 }, (err, result) => { - console.log(i, err, result); - if (++finished === 10) - pool.close(); - }); -} -``` - -### Integrating `AsyncResource` with `EventEmitter` - -Event listeners triggered by an [`EventEmitter`][] may be run in a different -execution context than the one that was active when `eventEmitter.on()` was -called. - -The following example shows how to use the `AsyncResource` class to properly -associate an event listener with the correct execution context. The same -approach can be applied to a [`Stream`][] or a similar event-driven class. - -```js -const { createServer } = require('http'); -const { AsyncResource, executionAsyncId } = require('async_hooks'); - -const server = createServer((req, res) => { - req.on('close', AsyncResource.bind(() => { - // Execution context is bound to the current outer scope. - })); - req.on('close', () => { - // Execution context is bound to the scope that caused 'close' to emit. - }); - res.end(); -}).listen(3000); -``` +The documentation for this class has moved [`AsyncResource`][]. ## Class: `AsyncLocalStorage` -<!-- YAML -added: - - v13.10.0 - - v12.17.0 ---> - -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. - -While you can create your own implementation on top of the `async_hooks` module, -`AsyncLocalStorage` should be preferred as it is a performant and memory safe -implementation that involves significant optimizations that are non-obvious to -implement. - -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 http = require('http'); -const { AsyncLocalStorage } = require('async_hooks'); - -const asyncLocalStorage = new AsyncLocalStorage(); - -function logWithId(msg) { - const id = asyncLocalStorage.getStore(); - console.log(`${id !== undefined ? id : '-'}:`, msg); -} - -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); - -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 -from each other. It is safe to instantiate this class multiple times. - -### `new AsyncLocalStorage()` -<!-- YAML -added: - - v13.10.0 - - v12.17.0 ---> - -Creates a new instance of `AsyncLocalStorage`. Store is only provided within a -`run()` call or after an `enterWith()` call. - -### `asyncLocalStorage.disable()` -<!-- YAML -added: - - v13.10.0 - - v12.17.0 ---> - -Disables the instance of `AsyncLocalStorage`. All subsequent calls -to `asyncLocalStorage.getStore()` will return `undefined` until -`asyncLocalStorage.run()` or `asyncLocalStorage.enterWith()` 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. - -Use this method when the `asyncLocalStorage` is not in use anymore -in the current process. - -### `asyncLocalStorage.getStore()` -<!-- YAML -added: - - v13.10.0 - - v12.17.0 ---> - -* Returns: {any} - -Returns the current store. -If called outside of an asynchronous context initialized by -calling `asyncLocalStorage.run()` or `asyncLocalStorage.enterWith()`, it -returns `undefined`. - -### `asyncLocalStorage.enterWith(store)` -<!-- YAML -added: - - v13.11.0 - - v12.17.0 ---> - -* `store` {any} - -Transitions into the context for the remainder of the current -synchronous execution and then persists the store through any following -asynchronous calls. - -Example: - -```js -const store = { id: 1 }; -// Replaces previous store with the given store object -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`. That is why -`run()` should be preferred over `enterWith()` unless there are strong reasons -to use the latter method. - -```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])` -<!-- YAML -added: - - v13.10.0 - - v12.17.0 ---> - -* `store` {any} -* `callback` {Function} -* `...args` {any} - -Runs a function synchronously within a context and returns its -return value. The store is not accessible outside of the callback function. -The store is accessible to any asynchronous operations created within the -callback. - -The optional `args` are passed to the callback function. - -If the callback function throws an error, the error is thrown by `run()` too. -The stacktrace is not impacted by this call and the context is exited. - -Example: - -```js -const store = { id: 2 }; -try { - asyncLocalStorage.run(store, () => { - asyncLocalStorage.getStore(); // Returns the store object - setTimeout(() => { - asyncLocalStorage.getStore(); // Returns the store object - }, 200); - throw new Error(); - }); -} catch (e) { - asyncLocalStorage.getStore(); // Returns undefined - // The error will be caught here -} -``` - -### `asyncLocalStorage.exit(callback[, ...args])` -<!-- YAML -added: - - v13.10.0 - - v12.17.0 ---> - -* `callback` {Function} -* `...args` {any} - -Runs a function synchronously outside of a context and returns its -return value. The store is not accessible within the callback function or -the asynchronous operations created within the callback. Any `getStore()` -call done within the callback function will always return `undefined`. - -The optional `args` are passed to the callback function. - -If the callback function throws an error, the error is thrown by `exit()` too. -The stacktrace is not impacted by this call and the context is re-entered. - -Example: - -```js -// Within a call to run -try { - asyncLocalStorage.getStore(); // Returns the store object or value - asyncLocalStorage.exit(() => { - asyncLocalStorage.getStore(); // Returns undefined - throw new Error(); - }); -} catch (e) { - asyncLocalStorage.getStore(); // Returns the same object or value - // The error will be caught here -} -``` - -### 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.run(new Map(), () => { - 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 `run`, calling `getStore` will return -`undefined`. - -### Troubleshooting - -In most cases your application or library code should have no issues with -`AsyncLocalStorage`. But in rare cases you may face situations when the -current store is lost in one of asynchronous operations. In those cases, -consider the following options. - -If your code is callback-based, it is enough to promisify it with -[`util.promisify()`][], so it starts working with native promises. -If you need to keep using callback-based API, or your code assumes -a custom thenable implementation, use the [`AsyncResource`][] class -to associate the asynchronous operation with the correct execution context. +The documentation for this class has moved [`AsyncLocalStorage`][]. [Hook Callbacks]: #async_hooks_hook_callbacks [PromiseHooks]: https://docs.google.com/document/d/1rda3yKGHimKIhg5YeoAmCOtyURgsbTH_qaYR79FELlk/edit -[`AsyncResource`]: #async_hooks_class_asyncresource +[`AsyncLocalStorage`]: async_context.md#async_context_class_asynclocalstorage +[`AsyncResource`]: async_context.md#async_context_class_asyncresource [`after` callback]: #async_hooks_after_asyncid [`before` callback]: #async_hooks_before_asyncid [`destroy` callback]: #async_hooks_destroy_asyncid [`init` callback]: #async_hooks_init_asyncid_type_triggerasyncid_resource [`promiseResolve` callback]: #async_hooks_promiseresolve_asyncid -[`EventEmitter`]: events.md#events_class_eventemitter -[`Stream`]: stream.md#stream_stream [`Worker`]: worker_threads.md#worker_threads_class_worker -[`util.promisify()`]: util.md#util_util_promisify_original [promise execution tracking]: #async_hooks_promise_execution_tracking |