Skip to content

Commit e29bd46

Browse files
Stephen Belangerdanielleadams
authored andcommitted
v8: multi-tenant promise hook api
PR-URL: #39283 Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent 3580286 commit e29bd46

12 files changed

+676
-10
lines changed

doc/api/v8.md

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,267 @@ added: v8.0.0
562562
A subclass of [`Deserializer`][] corresponding to the format written by
563563
[`DefaultSerializer`][].
564564

565+
## Promise hooks
566+
567+
The `promiseHooks` interface can be used to track promise lifecycle events.
568+
To track _all_ async activity, see [`async_hooks`][] which internally uses this
569+
module to produce promise lifecycle events in addition to events for other
570+
async resources. For request context management, see [`AsyncLocalStorage`][].
571+
572+
```mjs
573+
import { promiseHooks } from 'v8';
574+
575+
// There are four lifecycle events produced by promises:
576+
577+
// The `init` event represents the creation of a promise. This could be a
578+
// direct creation such as with `new Promise(...)` or a continuation such
579+
// as `then()` or `catch()`. It also happens whenever an async function is
580+
// called or does an `await`. If a continuation promise is created, the
581+
// `parent` will be the promise it is a continuation from.
582+
function init(promise, parent) {
583+
console.log('a promise was created', { promise, parent });
584+
}
585+
586+
// The `settled` event happens when a promise receives a resolution or
587+
// rejection value. This may happen synchronously such as when using
588+
// `Promise.resolve()` on non-promise input.
589+
function settled(promise) {
590+
console.log('a promise resolved or rejected', { promise });
591+
}
592+
593+
// The `before` event runs immediately before a `then()` or `catch()` handler
594+
// runs or an `await` resumes execution.
595+
function before(promise) {
596+
console.log('a promise is about to call a then handler', { promise });
597+
}
598+
599+
// The `after` event runs immediately after a `then()` handler runs or when
600+
// an `await` begins after resuming from another.
601+
function after(promise) {
602+
console.log('a promise is done calling a then handler', { promise });
603+
}
604+
605+
// Lifecycle hooks may be started and stopped individually
606+
const stopWatchingInits = promiseHooks.onInit(init);
607+
const stopWatchingSettleds = promiseHooks.onSettled(settled);
608+
const stopWatchingBefores = promiseHooks.onBefore(before);
609+
const stopWatchingAfters = promiseHooks.onAfter(after);
610+
611+
// Or they may be started and stopped in groups
612+
const stopHookSet = promiseHooks.createHook({
613+
init,
614+
settled,
615+
before,
616+
after
617+
});
618+
619+
// To stop a hook, call the function returned at its creation.
620+
stopWatchingInits();
621+
stopWatchingSettleds();
622+
stopWatchingBefores();
623+
stopWatchingAfters();
624+
stopHookSet();
625+
```
626+
627+
### `promiseHooks.onInit(init)`
628+
<!-- YAML
629+
added: REPLACEME
630+
-->
631+
632+
* `init` {Function} The [`init` callback][] to call when a promise is created.
633+
* Returns: {Function} Call to stop the hook.
634+
635+
**The `init` hook must be a plain function. Providing an async function will
636+
throw as it would produce an infinite microtask loop.**
637+
638+
```mjs
639+
import { promiseHooks } from 'v8';
640+
641+
const stop = promiseHooks.onInit((promise, parent) => {});
642+
```
643+
644+
```cjs
645+
const { promiseHooks } = require('v8');
646+
647+
const stop = promiseHooks.onInit((promise, parent) => {});
648+
```
649+
650+
### `promiseHooks.onSettled(settled)`
651+
<!-- YAML
652+
added: REPLACEME
653+
-->
654+
655+
* `settled` {Function} The [`settled` callback][] to call when a promise
656+
is resolved or rejected.
657+
* Returns: {Function} Call to stop the hook.
658+
659+
**The `settled` hook must be a plain function. Providing an async function will
660+
throw as it would produce an infinite microtask loop.**
661+
662+
```mjs
663+
import { promiseHooks } from 'v8';
664+
665+
const stop = promiseHooks.onSettled((promise) => {});
666+
```
667+
668+
```cjs
669+
const { promiseHooks } = require('v8');
670+
671+
const stop = promiseHooks.onSettled((promise) => {});
672+
```
673+
674+
### `promiseHooks.onBefore(before)`
675+
<!-- YAML
676+
added: REPLACEME
677+
-->
678+
679+
* `before` {Function} The [`before` callback][] to call before a promise
680+
continuation executes.
681+
* Returns: {Function} Call to stop the hook.
682+
683+
**The `before` hook must be a plain function. Providing an async function will
684+
throw as it would produce an infinite microtask loop.**
685+
686+
```mjs
687+
import { promiseHooks } from 'v8';
688+
689+
const stop = promiseHooks.onBefore((promise) => {});
690+
```
691+
692+
```cjs
693+
const { promiseHooks } = require('v8');
694+
695+
const stop = promiseHooks.onBefore((promise) => {});
696+
```
697+
698+
### `promiseHooks.onAfter(after)`
699+
<!-- YAML
700+
added: REPLACEME
701+
-->
702+
703+
* `after` {Function} The [`after` callback][] to call after a promise
704+
continuation executes.
705+
* Returns: {Function} Call to stop the hook.
706+
707+
**The `after` hook must be a plain function. Providing an async function will
708+
throw as it would produce an infinite microtask loop.**
709+
710+
```mjs
711+
import { promiseHooks } from 'v8';
712+
713+
const stop = promiseHooks.onAfter((promise) => {});
714+
```
715+
716+
```cjs
717+
const { promiseHooks } = require('v8');
718+
719+
const stop = promiseHooks.onAfter((promise) => {});
720+
```
721+
722+
### `promiseHooks.createHook(callbacks)`
723+
<!-- YAML
724+
added: REPLACEME
725+
-->
726+
727+
* `callbacks` {Object} The [Hook Callbacks][] to register
728+
* `init` {Function} The [`init` callback][].
729+
* `before` {Function} The [`before` callback][].
730+
* `after` {Function} The [`after` callback][].
731+
* `settled` {Function} The [`settled` callback][].
732+
* Returns: {Function} Used for disabling hooks
733+
734+
**The hook callbacks must be plain functions. Providing async functions will
735+
throw as it would produce an infinite microtask loop.**
736+
737+
Registers functions to be called for different lifetime events of each promise.
738+
739+
The callbacks `init()`/`before()`/`after()`/`settled()` are called for the
740+
respective events during a promise's lifetime.
741+
742+
All callbacks are optional. For example, if only promise creation needs to
743+
be tracked, then only the `init` callback needs to be passed. The
744+
specifics of all functions that can be passed to `callbacks` is in the
745+
[Hook Callbacks][] section.
746+
747+
```mjs
748+
import { promiseHooks } from 'v8';
749+
750+
const stopAll = promiseHooks.createHook({
751+
init(promise, parent) {}
752+
});
753+
```
754+
755+
```cjs
756+
const { promiseHooks } = require('v8');
757+
758+
const stopAll = promiseHooks.createHook({
759+
init(promise, parent) {}
760+
});
761+
```
762+
763+
### Hook callbacks
764+
765+
Key events in the lifetime of a promise have been categorized into four areas:
766+
creation of a promise, before/after a continuation handler is called or around
767+
an await, and when the promise resolves or rejects.
768+
769+
While these hooks are similar to those of [`async_hooks`][] they lack a
770+
`destroy` hook. Other types of async resources typically represent sockets or
771+
file descriptors which have a distinct "closed" state to express the `destroy`
772+
lifecycle event while promises remain usable for as long as code can still
773+
reach them. Garbage collection tracking is used to make promises fit into the
774+
`async_hooks` event model, however this tracking is very expensive and they may
775+
not necessarily ever even be garbage collected.
776+
777+
Because promises are asynchronous resources whose lifecycle is tracked
778+
via the promise hooks mechanism, the `init()`, `before()`, `after()`, and
779+
`settled()` callbacks *must not* be async functions as they create more
780+
promises which would produce an infinite loop.
781+
782+
While this API is used to feed promise events into [`async_hooks`][], the
783+
ordering between the two is considered undefined. Both APIs are multi-tenant
784+
and therefore could produce events in any order relative to each other.
785+
786+
#### `init(promise, parent)`
787+
788+
* `promise` {Promise} The promise being created.
789+
* `parent` {Promise} The promise continued from, if applicable.
790+
791+
Called when a promise is constructed. This _does not_ mean that corresponding
792+
`before`/`after` events will occur, only that the possibility exists. This will
793+
happen if a promise is created without ever getting a continuation.
794+
795+
#### `before(promise)`
796+
797+
* `promise` {Promise}
798+
799+
Called before a promise continuation executes. This can be in the form of
800+
`then()`, `catch()`, or `finally()` handlers or an `await` resuming.
801+
802+
The `before` callback will be called 0 to N times. The `before` callback
803+
will typically be called 0 times if no continuation was ever made for the
804+
promise. The `before` callback may be called many times in the case where
805+
many continuations have been made from the same promise.
806+
807+
#### `after(promise)`
808+
809+
* `promise` {Promise}
810+
811+
Called immediately after a promise continuation executes. This may be after a
812+
`then()`, `catch()`, or `finally()` handler or before an `await` after another
813+
`await`.
814+
815+
#### `settled(promise)`
816+
817+
* `promise` {Promise}
818+
819+
Called when the promise receives a resolution or rejection value. This may
820+
occur synchronously in the case of `Promise.resolve()` or `Promise.reject()`.
821+
565822
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
823+
[Hook Callbacks]: #hook_callbacks
566824
[V8]: https://developers.google.com/v8/
825+
[`AsyncLocalStorage`]: async_context.md#class_asynclocalstorage
567826
[`Buffer`]: buffer.md
568827
[`DefaultDeserializer`]: #class-v8defaultdeserializer
569828
[`DefaultSerializer`]: #class-v8defaultserializer
@@ -573,15 +832,20 @@ A subclass of [`Deserializer`][] corresponding to the format written by
573832
[`GetHeapSpaceStatistics`]: https://v8docs.nodesource.com/node-13.2/d5/dda/classv8_1_1_isolate.html#ac673576f24fdc7a33378f8f57e1d13a4
574833
[`NODE_V8_COVERAGE`]: cli.md#node_v8_coveragedir
575834
[`Serializer`]: #class-v8serializer
835+
[`after` callback]: #after_promise
836+
[`async_hooks`]: async_hooks.md
837+
[`before` callback]: #before_promise
576838
[`buffer.constants.MAX_LENGTH`]: buffer.md#bufferconstantsmax_length
577839
[`deserializer._readHostObject()`]: #deserializer_readhostobject
578840
[`deserializer.transferArrayBuffer()`]: #deserializertransferarraybufferid-arraybuffer
841+
[`init` callback]: #init_promise_parent
579842
[`serialize()`]: #v8serializevalue
580843
[`serializer._getSharedArrayBufferId()`]: #serializer_getsharedarraybufferidsharedarraybuffer
581844
[`serializer._writeHostObject()`]: #serializer_writehostobjectobject
582845
[`serializer.releaseBuffer()`]: #serializerreleasebuffer
583846
[`serializer.transferArrayBuffer()`]: #serializertransferarraybufferid-arraybuffer
584847
[`serializer.writeRawBytes()`]: #serializerwriterawbytesbuffer
848+
[`settled` callback]: #settled_promise
585849
[`v8.stopCoverage()`]: #v8stopcoverage
586850
[`v8.takeCoverage()`]: #v8takecoverage
587851
[`vm.Script`]: vm.md#new-vmscriptcode-options

lib/internal/async_hooks.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const {
88
Symbol,
99
} = primordials;
1010

11+
const promiseHooks = require('internal/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
@@ -51,8 +53,6 @@ const {
5153
executionAsyncResource: executionAsyncResource_,
5254
clearAsyncIdStack,
5355
} = async_wrap;
54-
// For performance reasons, only track Promises when a hook is enabled.
55-
const { setPromiseHooks } = async_wrap;
5656
// Properties in active_hooks are used to keep track of the set of hooks being
5757
// executed in case another hook is enabled/disabled. The new set of hooks is
5858
// then restored once the active set of hooks is finished executing.
@@ -374,6 +374,7 @@ function enableHooks() {
374374
async_hook_fields[kCheck] += 1;
375375
}
376376

377+
let stopPromiseHook;
377378
function updatePromiseHookMode() {
378379
wantPromiseHook = true;
379380
let initHook;
@@ -383,12 +384,13 @@ function updatePromiseHookMode() {
383384
} else if (destroyHooksExist()) {
384385
initHook = destroyTracking;
385386
}
386-
setPromiseHooks(
387-
initHook,
388-
promiseBeforeHook,
389-
promiseAfterHook,
390-
promiseResolveHooksExist() ? promiseResolveHook : undefined,
391-
);
387+
if (stopPromiseHook) stopPromiseHook();
388+
stopPromiseHook = promiseHooks.createHook({
389+
init: initHook,
390+
before: promiseBeforeHook,
391+
after: promiseAfterHook,
392+
settled: promiseResolveHooksExist() ? promiseResolveHook : undefined
393+
});
392394
}
393395

394396
function disableHooks() {
@@ -402,8 +404,8 @@ function disableHooks() {
402404
}
403405

404406
function disablePromiseHookIfNecessary() {
405-
if (!wantPromiseHook) {
406-
setPromiseHooks(undefined, undefined, undefined, undefined);
407+
if (!wantPromiseHook && stopPromiseHook) {
408+
stopPromiseHook();
407409
}
408410
}
409411

0 commit comments

Comments
 (0)