Skip to content

Commit 2e7497f

Browse files
author
Stephen Belanger
committed
async_hooks: multi-tenant promise hook api
1 parent c2e6822 commit 2e7497f

10 files changed

+471
-8
lines changed

doc/api/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
* [Performance hooks](perf_hooks.md)
4747
* [Policies](policy.md)
4848
* [Process](process.md)
49+
* [PromiseHooks](promise_hooks.md)
4950
* [Punycode](punycode.md)
5051
* [Query strings](querystring.md)
5152
* [Readline](readline.md)

doc/api/promise_hooks.md

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Promise hooks
2+
3+
<!--introduced_in=REPLACEME-->
4+
5+
> Stability: 1 - Experimental
6+
7+
<!-- source_link=lib/async_hooks.js -->
8+
9+
The `promiseHooks` API is part of the `async_hooks` module:
10+
11+
```mjs
12+
import { promiseHooks } from 'async_hooks';
13+
```
14+
15+
```cjs
16+
const { promiseHooks } = require('async_hooks');
17+
```
18+
19+
## Overview
20+
21+
Following is a simple overview of the public API.
22+
23+
```mjs
24+
import { promiseHooks } from 'async_hooks';
25+
26+
// There are four lifecycle events produced by promises:
27+
28+
// The `init` event represents the creation of a promise. This could be a
29+
// direct creation such as with `new Promise(...)` or a continuation such
30+
// as `then()` or `catch()`. It also happens whenever an async function is
31+
// called or does an `await`. If a continuation promise is created, the
32+
// `parent` will be the promise it is a continuation from.
33+
function init(promise, parent) {
34+
console.log('a promise was created', { promise, parent });
35+
}
36+
37+
// The `resolve` event happens when a promise receives a resolution or
38+
// rejection value. This may happen synchronously such as when using
39+
// `Promise.resolve()` on non-promise input.
40+
function resolve(promise) {
41+
console.log('a promise resolved or rejected', { promise });
42+
}
43+
44+
// The `before` event runs immediately before a `then()` handler runs or
45+
// an `await` resumes execution.
46+
function before(promise) {
47+
console.log('a promise is about to call a then handler', { promise });
48+
}
49+
50+
// The `after` event runs immediately after a `then()` handler runs or when
51+
// an `await` begins after resuming from another.
52+
function after(promise) {
53+
console.log('a promise is done calling a then handler', { promise });
54+
}
55+
56+
// Lifecycle hooks may be started and stopped individually
57+
const stopWatchingInits = promiseHooks.onInit(init);
58+
const stopWatchingResolves = promiseHooks.onResolve(resolve);
59+
const stopWatchingBefores = promiseHooks.onBefore(before);
60+
const stopWatchingAfters = promiseHooks.onAfter(after);
61+
62+
// Or they may be started and stopped in groups
63+
const stopAll = promiseHooks.createHook({
64+
init,
65+
resolve,
66+
before,
67+
after
68+
});
69+
70+
// To stop a hook, call the function returned at its creation.
71+
stopWatchingInits();
72+
stopWatchingResolves();
73+
stopWatchingBefores();
74+
stopWatchingAfters();
75+
stopAll();
76+
```
77+
78+
## `promiseHooks.createHook(callbacks)`
79+
80+
* `callbacks` {Object} The [Hook Callbacks][] to register
81+
* `init` {Function} The [`init` callback][].
82+
* `before` {Function} The [`before` callback][].
83+
* `after` {Function} The [`after` callback][].
84+
* `resolve` {Function} The [`resolve` callback][].
85+
* Returns: {Function} Used for disabling hooks
86+
87+
Registers functions to be called for different lifetime events of each promise.
88+
89+
The callbacks `init()`/`before()`/`after()`/`resolve()` are called for the
90+
respective events during a promise's lifetime.
91+
92+
All callbacks are optional. For example, if only promise creation needs to
93+
be tracked, then only the `init` callback needs to be passed. The
94+
specifics of all functions that can be passed to `callbacks` is in the
95+
[Hook Callbacks][] section.
96+
97+
```mjs
98+
import { promiseHooks } from 'async_hooks';
99+
100+
const stopAll = promiseHooks.createHook({
101+
init(promise, parent) {}
102+
});
103+
```
104+
105+
```cjs
106+
const { promiseHooks } = require('async_hooks');
107+
108+
const stopAll = promiseHooks.createHook({
109+
init(promise, parent) {}
110+
});
111+
```
112+
113+
### Hook callbacks
114+
115+
Key events in the lifetime of a promise have been categorized into four areas:
116+
creation of a promise, before/after a continuation handler is called or around
117+
an await, and when the promise resolves or rejects.
118+
119+
#### `init(promise, parent)`
120+
121+
* `promise` {Promise} The promise being created.
122+
* `parent` {Promise} The promise continued from, if applicable.
123+
124+
Called when a promise is constructed. This _does not_ mean that corresponding
125+
`before`/`after` events will occur, only that the possibility exists. This will
126+
happen if a promise is created without ever getting a continuation.
127+
128+
#### `before(promise)`
129+
130+
* `promise` {Promise}
131+
132+
Called before a promise continuation executes. This can be in the form of a
133+
`then()` handler or an `await` resuming.
134+
135+
The `before` callback will be called 0 to N times. The `before` callback
136+
will typically be called 0 times if no continuation was ever made for the
137+
promise. The `before` callback may be called many times in the case where
138+
many continuations have been made from the same promise.
139+
140+
#### `after(promise)`
141+
142+
* `promise` {Promise}
143+
144+
Called immediately after a promise continuation executes. This may be after a
145+
`then()` handler or before an `await` after another `await`.
146+
147+
#### `resolve(promise)`
148+
149+
* `promise` {Promise}
150+
151+
Called when the promise receives a resolution or rejection value. This may
152+
occur synchronously in the case of `Promise.resolve()` or `Promise.reject()`.
153+
154+
## `promiseHooks.onInit(init)`
155+
156+
* `init` {Function} The [`init` callback][] to call when a promise is created.
157+
* Returns: {Function} Call to stop the hook.
158+
159+
## `promiseHooks.onResolve(resolve)`
160+
161+
* `resolve` {Function} The [`resolve` callback][] to call when a promise
162+
is resolved or rejected.
163+
* Returns: {Function} Call to stop the hook.
164+
165+
## `promiseHooks.onBefore(before)`
166+
167+
* `before` {Function} The [`before` callback][] to call before a promise
168+
continuation executes.
169+
* Returns: {Function} Call to stop the hook.
170+
171+
## `promiseHooks.onAfter(after)`
172+
173+
* `after` {Function} The [`after` callback][] to call after a promise
174+
continuation executes.
175+
* Returns: {Function} Call to stop the hook.
176+
177+
[Hook Callbacks]: #promisehooks_hook_callbacks
178+
[`after` callback]: #promisehooks_after_promise
179+
[`before` callback]: #promisehooks_before_promise
180+
[`resolve` callback]: #promisehooks_resolve_promise
181+
[`init` callback]: #promisehooks_init_promise_parent

lib/internal/async_hooks.js

+14-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const {
88
Symbol,
99
} = primordials;
1010

11+
const PromiseHooks = require('internal/promise_hook');
12+
1113
const async_wrap = internalBinding('async_wrap');
1214
const { setCallbackTrampoline } = async_wrap;
1315
/* async_hook_fields is a Uint32Array wrapping the uint32_t array of
@@ -52,7 +54,7 @@ const {
5254
clearAsyncIdStack,
5355
} = async_wrap;
5456
// For performance reasons, only track Promises when a hook is enabled.
55-
const { enablePromiseHook, disablePromiseHook, setPromiseHooks } = async_wrap;
57+
const { enablePromiseHook, disablePromiseHook } = async_wrap;
5658
// Properties in active_hooks are used to keep track of the set of hooks being
5759
// executed in case another hook is enabled/disabled. The new set of hooks is
5860
// then restored once the active set of hooks is finished executing.
@@ -353,19 +355,21 @@ function enableHooks() {
353355
async_hook_fields[kCheck] += 1;
354356
}
355357

358+
let stopPromiseHook;
356359
function updatePromiseHookMode() {
357360
wantPromiseHook = true;
358361
if (destroyHooksExist()) {
359362
enablePromiseHook();
360363
setPromiseHooks(undefined, undefined, undefined, undefined);
361364
} else {
362365
disablePromiseHook();
363-
setPromiseHooks(
364-
initHooksExist() ? promiseInitHook : undefined,
365-
promiseBeforeHook,
366-
promiseAfterHook,
367-
promiseResolveHooksExist() ? promiseResolveHook : undefined,
368-
);
366+
if (stopPromiseHook) stopPromiseHook();
367+
stopPromiseHook = PromiseHooks.createHook({
368+
init: initHooksExist() ? promiseInitHook : undefined,
369+
before: promiseBeforeHook,
370+
after: promiseAfterHook,
371+
resolve: promiseResolveHooksExist() ? promiseResolveHook : undefined
372+
});
369373
}
370374
}
371375

@@ -382,7 +386,9 @@ function disableHooks() {
382386
function disablePromiseHookIfNecessary() {
383387
if (!wantPromiseHook) {
384388
disablePromiseHook();
385-
setPromiseHooks(undefined, undefined, undefined, undefined);
389+
if (stopPromiseHook) {
390+
stopPromiseHook();
391+
}
386392
}
387393
}
388394

lib/internal/promise_hook.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict';
2+
3+
const {
4+
ArrayPrototypeIndexOf,
5+
ArrayPrototypeSplice,
6+
ArrayPrototypePush,
7+
FunctionPrototypeBind
8+
} = primordials;
9+
10+
const { setPromiseHooks } = internalBinding('async_wrap');
11+
12+
const hooks = {
13+
init: [],
14+
before: [],
15+
after: [],
16+
resolve: []
17+
}
18+
19+
function initAll(promise, parent) {
20+
for (const init of hooks.init) {
21+
init(promise, parent)
22+
}
23+
}
24+
25+
function beforeAll(promise) {
26+
for (const before of hooks.before) {
27+
before(promise)
28+
}
29+
}
30+
31+
function afterAll(promise) {
32+
for (const after of hooks.after) {
33+
after(promise)
34+
}
35+
}
36+
37+
function resolveAll(promise) {
38+
for (const resolve of hooks.resolve) {
39+
resolve(promise)
40+
}
41+
}
42+
43+
function maybeFastPath(list, runAll) {
44+
return list.length > 1 ? runAll : list[0];
45+
}
46+
47+
function update() {
48+
const init = maybeFastPath(hooks.init, initAll);
49+
const before = maybeFastPath(hooks.before, beforeAll);
50+
const after = maybeFastPath(hooks.after, afterAll);
51+
const resolve = maybeFastPath(hooks.resolve, resolveAll);
52+
setPromiseHooks(init, before, after, resolve);
53+
}
54+
55+
function stop(list, hook) {
56+
const index = ArrayPrototypeIndexOf(list, hook);
57+
if (index >= 0) {
58+
ArrayPrototypeSplice(list, index, 1);
59+
update();
60+
}
61+
}
62+
63+
function makeUseHook(list) {
64+
return (hook) => {
65+
ArrayPrototypePush(list, hook);
66+
update();
67+
return FunctionPrototypeBind(stop, null, list, hook);
68+
}
69+
}
70+
71+
const onInit = makeUseHook(hooks.init);
72+
const onBefore = makeUseHook(hooks.before);
73+
const onAfter = makeUseHook(hooks.after);
74+
const onResolve = makeUseHook(hooks.resolve);
75+
76+
function createHook({ init, before, after, resolve } = {}) {
77+
const hooks = [];
78+
79+
if (init) ArrayPrototypePush(hooks, onInit(init));
80+
if (before) ArrayPrototypePush(hooks, onBefore(before));
81+
if (after) ArrayPrototypePush(hooks, onAfter(after));
82+
if (resolve) ArrayPrototypePush(hooks, onResolve(resolve));
83+
84+
return () => {
85+
for (const stop of hooks) {
86+
stop();
87+
}
88+
}
89+
}
90+
91+
module.exports = {
92+
createHook,
93+
onInit,
94+
onBefore,
95+
onAfter,
96+
onResolve
97+
};

test/parallel/test-bootstrap-modules.js

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const expectedModules = new Set([
9696
'NativeModule internal/process/signal',
9797
'NativeModule internal/process/task_queues',
9898
'NativeModule internal/process/warning',
99+
'NativeModule internal/promise_hook',
99100
'NativeModule internal/querystring',
100101
'NativeModule internal/source_map/source_map_cache',
101102
'NativeModule internal/stream_base_commons',

0 commit comments

Comments
 (0)