Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async_hooks: docs and api ergonomics tweaks #15103

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 86 additions & 47 deletions doc/api/async_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ function destroy(asyncId) { }
added: REPLACEME
-->

* `callbacks` {Object} the callbacks to register
* `callbacks` {Object} the [Hook Callbacks][] to register
* `init` {Function} The [`init` callback][].
* `before` {Function} The [`before` callback][].
* `after` {Function} The [`after` callback][].
* `destroy` {Function} The [`destroy` callback][].
* Returns: `{AsyncHook}` instance used for disabling and enabling hooks

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

```js
const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) { },
destroy(asyncId) { }
});
```

Note that the callbacks will be inherited via the prototype chain:

```js
class MyAsyncCallbacks {
init(asyncId, type, triggerAsyncId, resource) { }
destroy(asyncId) {}
}

class MyAddedCallbacks extends MyAsyncCallbacks {
before(asyncId) { }
after(asyncId) { }
}

const asyncHook = async_hooks.createHook(new MyAddedCallbacks());
```

##### Error Handling

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

Every new resource is assigned a unique ID.
Every new resource is assigned an ID that is unique within the scope of the
current process.

###### `type`

The `type` is a string that represents the type of resource that caused
The `type` is a string identifying the type of resource that caused
`init` to be called. Generally, it will correspond to the name of the
resource's constructor.

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

###### `triggerId`

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

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

###### `resource`

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

In the case of Promises, the `resource` object will have `promise` property
that refers to the Promise that is being initialized, and a `parentId` property
that equals the `asyncId` of a parent Promise, if there is one, and
`undefined` otherwise. For example, in the case of `b = a.then(handler)`,
`a` is considered a parent Promise of `b`.
set to the `asyncId` of a parent Promise, if there is one, and `undefined`
otherwise. For example, in the case of `b = a.then(handler)`, `a` is considered
a parent Promise of `b`.

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

###### asynchronous context example
###### Asynchronous context example
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure of any current style, but should headers capitalize all words? doesn't matter to me either way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heck if I know. I don't think we've had much of a consistent style here.


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

The `TCPWRAP` isn't part of this graph; even though it was the reason for
The `TCPWRAP` is not part of this graph; even though it was the reason for
`console.log()` being called. This is because binding to a port without a
hostname is actually synchronous, but to maintain a completely asynchronous API
the user's callback is placed in a `process.nextTick()`.
hostname is a *synchronous* operation within Node.js, but to maintain a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is "within Node.js" necessary? possibly we're referring to something else, but i'm not sure what.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely not necessary.

completely asynchronous API the user's callback is placed in a
`process.nextTick()`.

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

The `before` callback will be called 0 to N times. The `before` callback
will typically be called 0 times if the asynchronous operation was cancelled
or for example if no connections are received by a TCP server. Asynchronous
like the TCP server will typically call the `before` callback multiple times,
while other operations like `fs.open()` will only call it once.
or, for example, if no connections are received by a TCP server. Persistent
asynchronous resources like a TCP server will typically call the `before`
callback multiple times, while other operations like `fs.open()` will only call
it only once.


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

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


##### `destroy(asyncId)`

* `asyncId` {number}

Called after the resource corresponding to `asyncId` is destroyed. It is also called
asynchronously from the embedder API `emitDestroy()`.
Called after the resource corresponding to `asyncId` is destroyed. It is also
called asynchronously from the embedder API `emitDestroy()`.

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

#### `async_hooks.executionAsyncId()`

* Returns {number} the `asyncId` of the current execution context. Useful to track
when something calls.
* Returns {number} the `asyncId` of the current execution context. Useful to
track when something calls.

For example:

```js
const async_hooks = require('async_hooks');

console.log(async_hooks.executionAsyncId()); // 1 - bootstrap
fs.open(path, 'r', (err, fd) => {
console.log(async_hooks.executionAsyncId()); // 6 - open()
Expand Down Expand Up @@ -453,10 +489,9 @@ const server = net.createServer((conn) => {

## JavaScript Embedder API

Library developers that handle their own I/O, a connection pool, or
callback queues will need to hook into the AsyncWrap API so that all the
appropriate callbacks are called. To accommodate this a JavaScript API is
provided.
Library developers that handle their own asychronous resources performing tasks
like I/O, connection pooling, or managing callback queues may use the AsyncWrap
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should AsyncWrap be placed in backticks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

JavaScript API so that all the appropriate callbacks are called.

### `class AsyncResource()`

Expand All @@ -466,9 +501,9 @@ own resources.

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

It is important that `before`/`after` calls are unwound
*Note*: It is important that `before`/`after` calls are unwound
in the same order they are called. Otherwise an unrecoverable exception
will occur and node will abort.
will occur and the process will abort.

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

Expand Down Expand Up @@ -499,9 +534,9 @@ asyncResource.triggerAsyncId();
#### `AsyncResource(type[, triggerAsyncId])`

* arguments
* `type` {string} the type of ascyc event
* `triggerAsyncId` {number} the ID of the execution context that created this async
event
* `type` {string} the type of async event
* `triggerAsyncId` {number} the ID of the execution context that created this
async event

Example usage:

Expand Down Expand Up @@ -531,9 +566,9 @@ class DBQuery extends AsyncResource {

* Returns {undefined}

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

#### `asyncResource.emitAfter()`

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

If the user's callback throws an exception then `emitAfter()` will
automatically be called for all `asyncId`s on the stack if the error is handled by
a domain or `'uncaughtException'` handler.
If the user's callback throws an exception, `emitAfter()` will automatically be
called for all `asyncId`s on the stack if the error is handled by a domain or
`'uncaughtException'` handler.

#### `asyncResource.emitDestroy()`

Expand All @@ -564,4 +599,8 @@ never be called.
* Returns {number} the same `triggerAsyncId` that is passed to the `AsyncResource`
constructor.

[`after` callback]: #async_hooks_after_asyncid
[`before` callback]: #async_hooks_before_asyncid
[`destroy` callback]: #async_hooks_before_asyncid
[`Hook Callbacks`]: #async_hooks_hook_callbacks
[`init` callback]: #async_hooks_init_asyncid_type_triggerasyncid_resource
3 changes: 3 additions & 0 deletions lib/async_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ function triggerAsyncId() {

class AsyncResource {
constructor(type, triggerAsyncId = initTriggerId()) {
if (typeof type !== 'string')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'type', 'string');

// Unlike emitInitScript, AsyncResource doesn't supports null as the
// triggerAsyncId.
if (!Number.isSafeInteger(triggerAsyncId) || triggerAsyncId < -1) {
Expand Down
34 changes: 34 additions & 0 deletions test/async-hooks/test-embedder.api.async-resource-no-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const { AsyncResource } = async_hooks;
const { spawn } = require('child_process');

const initHooks = require('./init-hooks');

if (process.argv[2] === 'child') {
initHooks().enable();

class Foo extends AsyncResource {
constructor(type) {
super(type, async_hooks.executionAsyncId());
}
}

[null, undefined, 1, Date, {}, []].forEach((i) => {
common.expectsError(() => new Foo(i), {
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError
});
});

} else {
const args = process.argv.slice(1).concat('child');
spawn(process.execPath, args)
.on('close', common.mustCall((code) => {
// No error because the type was defaulted
assert.strictEqual(code, 0);
}));
}
11 changes: 5 additions & 6 deletions test/async-hooks/test-embedder.api.async-resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ const { checkInvocations } = require('./hook-checks');
const hooks = initHooks();
hooks.enable();

assert.throws(() => {
new AsyncResource();
}, common.expectsError({
code: 'ERR_ASYNC_TYPE',
type: TypeError,
}));
common.expectsError(
() => new AsyncResource(), {
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
});
assert.throws(() => {
new AsyncResource('invalid_trigger_id', null);
}, common.expectsError({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ async_hooks.createHook({
assert.throws(() => {
return new AsyncResource();
}, common.expectsError({
code: 'ERR_ASYNC_TYPE',
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
}));

Expand Down