Skip to content

Commit d8a0364

Browse files
committed
async_hooks,doc: some async_hooks improvements
Update docs and type checking for AsyncResource type PR-URL: #15103 Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Andreas Madsen <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Trevor Norris <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 8f52ccc commit d8a0364

File tree

5 files changed

+128
-54
lines changed

5 files changed

+128
-54
lines changed

doc/api/async_hooks.md

Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ function destroy(asyncId) { }
7373
added: REPLACEME
7474
-->
7575

76-
* `callbacks` {Object} the callbacks to register
76+
* `callbacks` {Object} the [Hook Callbacks][] to register
77+
* `init` {Function} The [`init` callback][].
78+
* `before` {Function} The [`before` callback][].
79+
* `after` {Function} The [`after` callback][].
80+
* `destroy` {Function} The [`destroy` callback][].
7781
* Returns: `{AsyncHook}` instance used for disabling and enabling hooks
7882

7983
Registers functions to be called for different lifetime events of each async
@@ -87,6 +91,31 @@ be tracked then only the `destroy` callback needs to be passed. The
8791
specifics of all functions that can be passed to `callbacks` is in the section
8892
[`Hook Callbacks`][].
8993

94+
```js
95+
const async_hooks = require('async_hooks');
96+
97+
const asyncHook = async_hooks.createHook({
98+
init(asyncId, type, triggerAsyncId, resource) { },
99+
destroy(asyncId) { }
100+
});
101+
```
102+
103+
Note that the callbacks will be inherited via the prototype chain:
104+
105+
```js
106+
class MyAsyncCallbacks {
107+
init(asyncId, type, triggerAsyncId, resource) { }
108+
destroy(asyncId) {}
109+
}
110+
111+
class MyAddedCallbacks extends MyAsyncCallbacks {
112+
before(asyncId) { }
113+
after(asyncId) { }
114+
}
115+
116+
const asyncHook = async_hooks.createHook(new MyAddedCallbacks());
117+
```
118+
90119
##### Error Handling
91120

92121
If any `AsyncHook` callbacks throw, the application will print the stack trace
@@ -187,11 +216,12 @@ require('net').createServer().listen(function() { this.close(); });
187216
clearTimeout(setTimeout(() => {}, 10));
188217
```
189218

190-
Every new resource is assigned a unique ID.
219+
Every new resource is assigned an ID that is unique within the scope of the
220+
current process.
191221

192222
###### `type`
193223

194-
The `type` is a string that represents the type of resource that caused
224+
The `type` is a string identifying the type of resource that caused
195225
`init` to be called. Generally, it will correspond to the name of the
196226
resource's constructor.
197227

@@ -214,8 +244,8 @@ when listening to the hooks.
214244

215245
###### `triggerId`
216246

217-
`triggerAsyncId` is the `asyncId` of the resource that caused (or "triggered") the
218-
new resource to initialize and that caused `init` to call. This is different
247+
`triggerAsyncId` is the `asyncId` of the resource that caused (or "triggered")
248+
the new resource to initialize and that caused `init` to call. This is different
219249
from `async_hooks.executionAsyncId()` that only shows *when* a resource was
220250
created, while `triggerAsyncId` shows *why* a resource was created.
221251

@@ -253,26 +283,27 @@ propagating what resource is responsible for the new resource's existence.
253283

254284
###### `resource`
255285

256-
`resource` is an object that represents the actual resource. This can contain
257-
useful information such as the hostname for the `GETADDRINFOREQWRAP` resource
258-
type, which will be used when looking up the ip for the hostname in
259-
`net.Server.listen`. The API for getting this information is currently not
260-
considered public, but using the Embedder API users can provide and document
261-
their own resource objects. Such as resource object could for example contain
262-
the SQL query being executed.
286+
`resource` is an object that represents the actual async resource that has
287+
been initialized. This can contain useful information that can vary based on
288+
the value of `type`. For instance, for the `GETADDRINFOREQWRAP` resource type,
289+
`resource` provides the hostname used when looking up the IP address for the
290+
hostname in `net.Server.listen()`. The API for accessing this information is
291+
currently not considered public, but using the Embedder API, users can provide
292+
and document their own resource objects. Such a resource object could for
293+
example contain the SQL query being executed.
263294

264295
In the case of Promises, the `resource` object will have `promise` property
265296
that refers to the Promise that is being initialized, and a `parentId` property
266-
that equals the `asyncId` of a parent Promise, if there is one, and
267-
`undefined` otherwise. For example, in the case of `b = a.then(handler)`,
268-
`a` is considered a parent Promise of `b`.
297+
set to the `asyncId` of a parent Promise, if there is one, and `undefined`
298+
otherwise. For example, in the case of `b = a.then(handler)`, `a` is considered
299+
a parent Promise of `b`.
269300

270301
*Note*: In some cases the resource object is reused for performance reasons,
271302
it is thus not safe to use it as a key in a `WeakMap` or add properties to it.
272303

273-
###### asynchronous context example
304+
###### Asynchronous context example
274305

275-
Below is another example with additional information about the calls to
306+
The following is an example with additional information about the calls to
276307
`init` between the `before` and `after` calls, specifically what the
277308
callback to `listen()` will look like. The output formatting is slightly more
278309
elaborate to make calling context easier to see.
@@ -348,10 +379,10 @@ Only using `execution` to graph resource allocation results in the following:
348379
TTYWRAP(6) -> Timeout(4) -> TIMERWRAP(5) -> TickObject(3) -> root(1)
349380
```
350381

351-
The `TCPWRAP` isn't part of this graph; even though it was the reason for
382+
The `TCPWRAP` is not part of this graph; even though it was the reason for
352383
`console.log()` being called. This is because binding to a port without a
353-
hostname is actually synchronous, but to maintain a completely asynchronous API
354-
the user's callback is placed in a `process.nextTick()`.
384+
hostname is a *synchronous* operation, but to maintain a completely asynchronous
385+
API the user's callback is placed in a `process.nextTick()`.
355386

356387
The graph only shows *when* a resource was created, not *why*, so to track
357388
the *why* use `triggerAsyncId`.
@@ -369,9 +400,10 @@ resource about to execute the callback.
369400

370401
The `before` callback will be called 0 to N times. The `before` callback
371402
will typically be called 0 times if the asynchronous operation was cancelled
372-
or for example if no connections are received by a TCP server. Asynchronous
373-
like the TCP server will typically call the `before` callback multiple times,
374-
while other operations like `fs.open()` will only call it once.
403+
or, for example, if no connections are received by a TCP server. Persistent
404+
asynchronous resources like a TCP server will typically call the `before`
405+
callback multiple times, while other operations like `fs.open()` will only call
406+
it only once.
375407

376408

377409
##### `after(asyncId)`
@@ -381,30 +413,33 @@ while other operations like `fs.open()` will only call it once.
381413
Called immediately after the callback specified in `before` is completed.
382414

383415
*Note:* If an uncaught exception occurs during execution of the callback then
384-
`after` will run after the `'uncaughtException'` event is emitted or a
416+
`after` will run *after* the `'uncaughtException'` event is emitted or a
385417
`domain`'s handler runs.
386418

387419

388420
##### `destroy(asyncId)`
389421

390422
* `asyncId` {number}
391423

392-
Called after the resource corresponding to `asyncId` is destroyed. It is also called
393-
asynchronously from the embedder API `emitDestroy()`.
424+
Called after the resource corresponding to `asyncId` is destroyed. It is also
425+
called asynchronously from the embedder API `emitDestroy()`.
394426

395-
*Note:* Some resources depend on GC for cleanup, so if a reference is made to
396-
the `resource` object passed to `init` it's possible that `destroy` is
397-
never called, causing a memory leak in the application. Of course if
398-
the resource doesn't depend on GC then this isn't an issue.
427+
*Note:* Some resources depend on garbage collection for cleanup, so if a
428+
reference is made to the `resource` object passed to `init` it is possible that
429+
`destroy` will never be called, causing a memory leak in the application. If
430+
the resource does not depend on garbage collection, then this will not be an
431+
issue.
399432

400433
#### `async_hooks.executionAsyncId()`
401434

402-
* Returns {number} the `asyncId` of the current execution context. Useful to track
403-
when something calls.
435+
* Returns {number} the `asyncId` of the current execution context. Useful to
436+
track when something calls.
404437

405438
For example:
406439

407440
```js
441+
const async_hooks = require('async_hooks');
442+
408443
console.log(async_hooks.executionAsyncId()); // 1 - bootstrap
409444
fs.open(path, 'r', (err, fd) => {
410445
console.log(async_hooks.executionAsyncId()); // 6 - open()
@@ -453,10 +488,9 @@ const server = net.createServer((conn) => {
453488

454489
## JavaScript Embedder API
455490

456-
Library developers that handle their own I/O, a connection pool, or
457-
callback queues will need to hook into the AsyncWrap API so that all the
458-
appropriate callbacks are called. To accommodate this a JavaScript API is
459-
provided.
491+
Library developers that handle their own asychronous resources performing tasks
492+
like I/O, connection pooling, or managing callback queues may use the `AsyncWrap`
493+
JavaScript API so that all the appropriate callbacks are called.
460494

461495
### `class AsyncResource()`
462496

@@ -466,9 +500,9 @@ own resources.
466500

467501
The `init` hook will trigger when an `AsyncResource` is instantiated.
468502

469-
It is important that `before`/`after` calls are unwound
503+
*Note*: It is important that `before`/`after` calls are unwound
470504
in the same order they are called. Otherwise an unrecoverable exception
471-
will occur and node will abort.
505+
will occur and the process will abort.
472506

473507
The following is an overview of the `AsyncResource` API.
474508

@@ -499,9 +533,9 @@ asyncResource.triggerAsyncId();
499533
#### `AsyncResource(type[, triggerAsyncId])`
500534

501535
* arguments
502-
* `type` {string} the type of ascyc event
503-
* `triggerAsyncId` {number} the ID of the execution context that created this async
504-
event
536+
* `type` {string} the type of async event
537+
* `triggerAsyncId` {number} the ID of the execution context that created this
538+
async event
505539

506540
Example usage:
507541

@@ -531,9 +565,9 @@ class DBQuery extends AsyncResource {
531565

532566
* Returns {undefined}
533567

534-
Call all `before` callbacks and let them know a new asynchronous execution
535-
context is being entered. If nested calls to `emitBefore()` are made, the stack
536-
of `asyncId`s will be tracked and properly unwound.
568+
Call all `before` callbacks to notify that a new asynchronous execution context
569+
is being entered. If nested calls to `emitBefore()` are made, the stack of
570+
`asyncId`s will be tracked and properly unwound.
537571

538572
#### `asyncResource.emitAfter()`
539573

@@ -542,9 +576,9 @@ of `asyncId`s will be tracked and properly unwound.
542576
Call all `after` callbacks. If nested calls to `emitBefore()` were made, then
543577
make sure the stack is unwound properly. Otherwise an error will be thrown.
544578

545-
If the user's callback throws an exception then `emitAfter()` will
546-
automatically be called for all `asyncId`s on the stack if the error is handled by
547-
a domain or `'uncaughtException'` handler.
579+
If the user's callback throws an exception, `emitAfter()` will automatically be
580+
called for all `asyncId`s on the stack if the error is handled by a domain or
581+
`'uncaughtException'` handler.
548582

549583
#### `asyncResource.emitDestroy()`
550584

@@ -564,4 +598,8 @@ never be called.
564598
* Returns {number} the same `triggerAsyncId` that is passed to the `AsyncResource`
565599
constructor.
566600

601+
[`after` callback]: #async_hooks_after_asyncid
602+
[`before` callback]: #async_hooks_before_asyncid
603+
[`destroy` callback]: #async_hooks_before_asyncid
567604
[`Hook Callbacks`]: #async_hooks_hook_callbacks
605+
[`init` callback]: #async_hooks_init_asyncid_type_triggerasyncid_resource

lib/async_hooks.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ function triggerAsyncId() {
234234

235235
class AsyncResource {
236236
constructor(type, triggerAsyncId = initTriggerId()) {
237+
if (typeof type !== 'string')
238+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'type', 'string');
239+
237240
// Unlike emitInitScript, AsyncResource doesn't supports null as the
238241
// triggerAsyncId.
239242
if (!Number.isSafeInteger(triggerAsyncId) || triggerAsyncId < -1) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const async_hooks = require('async_hooks');
6+
const { AsyncResource } = async_hooks;
7+
const { spawn } = require('child_process');
8+
9+
const initHooks = require('./init-hooks');
10+
11+
if (process.argv[2] === 'child') {
12+
initHooks().enable();
13+
14+
class Foo extends AsyncResource {
15+
constructor(type) {
16+
super(type, async_hooks.executionAsyncId());
17+
}
18+
}
19+
20+
[null, undefined, 1, Date, {}, []].forEach((i) => {
21+
common.expectsError(() => new Foo(i), {
22+
code: 'ERR_INVALID_ARG_TYPE',
23+
type: TypeError
24+
});
25+
});
26+
27+
} else {
28+
const args = process.argv.slice(1).concat('child');
29+
spawn(process.execPath, args)
30+
.on('close', common.mustCall((code) => {
31+
// No error because the type was defaulted
32+
assert.strictEqual(code, 0);
33+
}));
34+
}

test/async-hooks/test-embedder.api.async-resource.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ const { checkInvocations } = require('./hook-checks');
1212
const hooks = initHooks();
1313
hooks.enable();
1414

15-
assert.throws(() => {
16-
new AsyncResource();
17-
}, common.expectsError({
18-
code: 'ERR_ASYNC_TYPE',
19-
type: TypeError,
20-
}));
15+
common.expectsError(
16+
() => new AsyncResource(), {
17+
code: 'ERR_INVALID_ARG_TYPE',
18+
type: TypeError,
19+
});
2120
assert.throws(() => {
2221
new AsyncResource('invalid_trigger_id', null);
2322
}, common.expectsError({

test/parallel/test-async-hooks-asyncresource-constructor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async_hooks.createHook({
1515
assert.throws(() => {
1616
return new AsyncResource();
1717
}, common.expectsError({
18-
code: 'ERR_ASYNC_TYPE',
18+
code: 'ERR_INVALID_ARG_TYPE',
1919
type: TypeError,
2020
}));
2121

0 commit comments

Comments
 (0)