Skip to content

Commit e6d77f3

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

10 files changed

+497
-9
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

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

lib/internal/async_hooks.js

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

11+
const PromiseHooks = require('promise_hooks');
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,20 @@ function enableHooks() {
353355
async_hook_fields[kCheck] += 1;
354356
}
355357

358+
let stopPromiseHook;
356359
function updatePromiseHookMode() {
357360
wantPromiseHook = true;
361+
if (stopPromiseHook) stopPromiseHook();
358362
if (destroyHooksExist()) {
359363
enablePromiseHook();
360-
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+
stopPromiseHook = PromiseHooks.createHook({
367+
init: initHooksExist() ? promiseInitHook : undefined,
368+
before: promiseBeforeHook,
369+
after: promiseAfterHook,
370+
resolve: promiseResolveHooksExist() ? promiseResolveHook : undefined
371+
});
369372
}
370373
}
371374

@@ -382,7 +385,9 @@ function disableHooks() {
382385
function disablePromiseHookIfNecessary() {
383386
if (!wantPromiseHook) {
384387
disablePromiseHook();
385-
setPromiseHooks(undefined, undefined, undefined, undefined);
388+
if (stopPromiseHook) {
389+
stopPromiseHook();
390+
}
386391
}
387392
}
388393

lib/promise_hooks.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
@@ -128,6 +128,7 @@ const expectedModules = new Set([
128128
'NativeModule async_hooks',
129129
'NativeModule path',
130130
'NativeModule perf_hooks',
131+
'NativeModule promise_hooks',
131132
'NativeModule querystring',
132133
'NativeModule stream',
133134
'NativeModule stream/promises',

0 commit comments

Comments
 (0)