Skip to content

Commit db47dbb

Browse files
committed
fixup! async_hooks: runtime deprecate createHook()
Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 2fb146d commit db47dbb

5 files changed

Lines changed: 155 additions & 57 deletions

File tree

‎lib/async_hooks.js‎

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeIncludes,
5+
ArrayPrototypeIndexOf,
6+
ArrayPrototypePush,
7+
ArrayPrototypeSplice,
48
ArrayPrototypeUnshift,
59
FunctionPrototypeBind,
610
NumberIsSafeInteger,
@@ -11,15 +15,18 @@ const {
1115
} = primordials;
1216

1317
const {
18+
ERR_ASYNC_CALLBACK,
1419
ERR_ASYNC_RESOURCE_DOMAIN_REMOVED,
1520
ERR_ASYNC_TYPE,
1621
ERR_INVALID_ASYNC_ID,
22+
ERR_INVALID_ARG_VALUE,
1723
} = require('internal/errors').codes;
1824
const {
1925
deprecate,
2026
kEmptyObject,
2127
} = require('internal/util');
2228
const {
29+
validateBoolean,
2330
validateFunction,
2431
validateString,
2532
} = require('internal/validators');
@@ -30,13 +37,17 @@ const AsyncContextFrame = require('internal/async_context_frame');
3037
// Get functions
3138
// For userland AsyncResources, make sure to emit a destroy event when the
3239
// resource gets gced.
33-
const { registerDestroyHook } = internal_async_hooks;
40+
const { registerDestroyHook, kNoPromiseHook } = internal_async_hooks;
3441
const {
3542
asyncWrap,
3643
executionAsyncId,
3744
triggerAsyncId,
3845
// Private API
3946
hasAsyncIdStack,
47+
getHookArrays,
48+
enableHooks,
49+
disableHooks,
50+
updatePromiseHookMode,
4051
executionAsyncResource,
4152
// Internal Embedder API
4253
newAsyncId,
@@ -52,12 +63,115 @@ const {
5263
// Get symbols
5364
const {
5465
async_id_symbol, trigger_async_id_symbol,
66+
init_symbol, before_symbol, after_symbol, destroy_symbol,
67+
promise_resolve_symbol,
5568
} = internal_async_hooks.symbols;
5669

70+
// Get constants
71+
const {
72+
kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,
73+
} = internal_async_hooks.constants;
74+
5775
// Listener API //
5876

77+
class AsyncHook {
78+
constructor({ init, before, after, destroy, promiseResolve, trackPromises }) {
79+
if (init !== undefined && typeof init !== 'function')
80+
throw new ERR_ASYNC_CALLBACK('hook.init');
81+
if (before !== undefined && typeof before !== 'function')
82+
throw new ERR_ASYNC_CALLBACK('hook.before');
83+
if (after !== undefined && typeof after !== 'function')
84+
throw new ERR_ASYNC_CALLBACK('hook.after');
85+
if (destroy !== undefined && typeof destroy !== 'function')
86+
throw new ERR_ASYNC_CALLBACK('hook.destroy');
87+
if (promiseResolve !== undefined && typeof promiseResolve !== 'function')
88+
throw new ERR_ASYNC_CALLBACK('hook.promiseResolve');
89+
if (trackPromises !== undefined) {
90+
validateBoolean(trackPromises, 'trackPromises');
91+
}
92+
93+
this[init_symbol] = init;
94+
this[before_symbol] = before;
95+
this[after_symbol] = after;
96+
this[destroy_symbol] = destroy;
97+
this[promise_resolve_symbol] = promiseResolve;
98+
if (trackPromises === false) {
99+
if (promiseResolve) {
100+
throw new ERR_INVALID_ARG_VALUE('trackPromises',
101+
trackPromises, 'must not be false when promiseResolve is enabled');
102+
}
103+
this[kNoPromiseHook] = true;
104+
} else {
105+
// Default to tracking promises for now.
106+
this[kNoPromiseHook] = false;
107+
}
108+
}
109+
110+
enable() {
111+
// The set of callbacks for a hook should be the same regardless of whether
112+
// enable()/disable() are run during their execution. The following
113+
// references are reassigned to the tmp arrays if a hook is currently being
114+
// processed.
115+
const { 0: hooks_array, 1: hook_fields } = getHookArrays();
116+
117+
// Each hook is only allowed to be added once.
118+
if (ArrayPrototypeIncludes(hooks_array, this))
119+
return this;
120+
121+
const prev_kTotals = hook_fields[kTotals];
122+
123+
// createHook() has already enforced that the callbacks are all functions,
124+
// so here simply increment the count of whether each callbacks exists or
125+
// not.
126+
hook_fields[kTotals] = hook_fields[kInit] += +!!this[init_symbol];
127+
hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
128+
hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
129+
hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
130+
hook_fields[kTotals] +=
131+
hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol];
132+
ArrayPrototypePush(hooks_array, this);
133+
134+
if (prev_kTotals === 0 && hook_fields[kTotals] > 0) {
135+
enableHooks();
136+
}
137+
138+
if (!this[kNoPromiseHook]) {
139+
updatePromiseHookMode();
140+
}
141+
142+
return this;
143+
}
144+
145+
disable() {
146+
const { 0: hooks_array, 1: hook_fields } = getHookArrays();
147+
148+
const index = ArrayPrototypeIndexOf(hooks_array, this);
149+
if (index === -1)
150+
return this;
151+
152+
const prev_kTotals = hook_fields[kTotals];
153+
154+
hook_fields[kTotals] = hook_fields[kInit] -= +!!this[init_symbol];
155+
hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol];
156+
hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol];
157+
hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
158+
hook_fields[kTotals] +=
159+
hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol];
160+
ArrayPrototypeSplice(hooks_array, index, 1);
161+
162+
if (prev_kTotals > 0 && hook_fields[kTotals] === 0) {
163+
disableHooks();
164+
}
165+
166+
return this;
167+
}
168+
}
169+
170+
59171
const createHook = deprecate(
60-
internal_async_hooks.createHook,
172+
function createHook(fns) {
173+
return new AsyncHook(fns);
174+
},
61175
'async_hooks.createHook() is deprecated. Use AsyncLocalStorage for async ' +
62176
'context tracking and diagnostics_channel for tracing instead.',
63177
'DEP0212',

‎lib/internal/async_hooks.js‎

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@ const {
1313
Symbol,
1414
} = primordials;
1515

16-
const {
17-
ERR_ASYNC_CALLBACK,
18-
ERR_INVALID_ARG_VALUE,
19-
} = require('internal/errors').codes;
20-
const { validateBoolean } = require('internal/validators');
21-
2216
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
2317

2418
const async_wrap = internalBinding('async_wrap');
@@ -586,86 +580,63 @@ function triggerAsyncId() {
586580

587581
const kNoPromiseHook = Symbol('kNoPromiseHook');
588582

589-
class AsyncHookForInspector {
590-
constructor({ init, before, after, destroy }) {
583+
// A minimal AsyncHook for the inspector and the node.async_hooks trace event
584+
// category, so that they do not go through the deprecated public
585+
// async_hooks.createHook(). It does not validate its arguments, does not
586+
// support promiseResolve, and expects init, before, after and destroy to all
587+
// be functions.
588+
class InternalAsyncHook {
589+
constructor({ init, before, after, destroy, trackPromises = true }) {
591590
this[init_symbol] = init;
592591
this[before_symbol] = before;
593592
this[after_symbol] = after;
594593
this[destroy_symbol] = destroy;
595-
this[promise_resolve_symbol] = promiseResolve;
596-
this[kNoPromiseHook] = true;
594+
this[kNoPromiseHook] = !trackPromises;
597595
}
598596

599597
enable() {
600-
// The set of callbacks for a hook should be the same regardless of whether
601-
// enable()/disable() are run during their execution. The following
602-
// references are reassigned to the tmp arrays if a hook is currently being
603-
// processed.
604598
const { 0: hooks_array, 1: hook_fields } = getHookArrays();
605-
606-
// Each hook is only allowed to be added once.
607599
if (ArrayPrototypeIncludes(hooks_array, this))
608600
return this;
609601

610602
const prev_kTotals = hook_fields[kTotals];
611-
612-
// createHook() has already enforced that the callbacks are all functions,
613-
// so here simply increment the count of whether each callbacks exists or
614-
// not.
615-
hook_fields[kTotals] = hook_fields[kInit] += +!!this[init_symbol];
616-
hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
617-
hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
618-
hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
619-
hook_fields[kTotals] +=
620-
hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol];
603+
hook_fields[kTotals] = hook_fields[kInit] += 1;
604+
hook_fields[kTotals] += hook_fields[kBefore] += 1;
605+
hook_fields[kTotals] += hook_fields[kAfter] += 1;
606+
hook_fields[kTotals] += hook_fields[kDestroy] += 1;
607+
hook_fields[kTotals] += hook_fields[kPromiseResolve];
621608
ArrayPrototypePush(hooks_array, this);
622609

623-
if (prev_kTotals === 0 && hook_fields[kTotals] > 0) {
610+
if (prev_kTotals === 0)
624611
enableHooks();
625-
}
626-
627-
if (!this[kNoPromiseHook]) {
612+
if (!this[kNoPromiseHook])
628613
updatePromiseHookMode();
629-
}
630-
631614
return this;
632615
}
633616

634617
disable() {
635618
const { 0: hooks_array, 1: hook_fields } = getHookArrays();
636-
637619
const index = ArrayPrototypeIndexOf(hooks_array, this);
638620
if (index === -1)
639621
return this;
640622

641-
const prev_kTotals = hook_fields[kTotals];
642-
643-
hook_fields[kTotals] = hook_fields[kInit] -= +!!this[init_symbol];
644-
hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol];
645-
hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol];
646-
hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
647-
hook_fields[kTotals] +=
648-
hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol];
623+
hook_fields[kTotals] = hook_fields[kInit] -= 1;
624+
hook_fields[kTotals] += hook_fields[kBefore] -= 1;
625+
hook_fields[kTotals] += hook_fields[kAfter] -= 1;
626+
hook_fields[kTotals] += hook_fields[kDestroy] -= 1;
627+
hook_fields[kTotals] += hook_fields[kPromiseResolve];
649628
ArrayPrototypeSplice(hooks_array, index, 1);
650629

651-
if (prev_kTotals > 0 && hook_fields[kTotals] === 0) {
630+
if (hook_fields[kTotals] === 0)
652631
disableHooks();
653-
}
654-
655632
return this;
656633
}
657634
}
658635

659-
// Used by internal consumers (the inspector and trace events) so that they do
660-
// not trigger the deprecation warning of the public async_hooks.createHook().
661-
function createHook(fns) {
662-
return new AsyncHookForInspector(fns);
663-
}
664-
665636
module.exports = {
666637
executionAsyncId,
667638
triggerAsyncId,
668-
createHook,
639+
InternalAsyncHook,
669640
// Private API
670641
getHookArrays,
671642
symbols: {

‎lib/internal/inspector_async_hook.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ let config;
55

66
function lazyHookCreation() {
77
const inspector = internalBinding('inspector');
8-
const { createHook } = require('internal/async_hooks');
8+
const { InternalAsyncHook } = require('internal/async_hooks');
99
config = internalBinding('config');
1010

11-
hook = createHook({
11+
hook = new InternalAsyncHook({
1212
init(asyncId, type, triggerAsyncId, resource) {
1313
// It's difficult to tell which tasks will be recurring and which won't,
1414
// therefore we mark all tasks as recurring. Based on the discussion

‎lib/internal/trace_events_async_hooks.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function createHook() {
3838
// repeated. Since async_hooks doesn't expose the provider type in the
3939
// non-init events, use a map to manually map the asyncId to the type name.
4040

41-
const hook = async_hooks.createHook({
41+
const hook = new async_hooks.InternalAsyncHook({
4242
init(asyncId, type, triggerAsyncId, resource) {
4343
if (nativeProviders.has(type)) return;
4444

‎test/trace_events/test-trace-events-async-hooks.js‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const {
1515
checkTraceProcessor();
1616

1717
const CODE =
18-
'setTimeout(() => { for (let i = 0; i < 100000; i++) { "test" + i } }, 1)';
18+
'setTimeout(() => { for (let i = 0; i < 100000; i++) { "test" + i } }, 1);' +
19+
'Promise.resolve().then(() => {});';
1920

2021
const tmpdir = require('../common/tmpdir');
2122
tmpdir.refresh();
@@ -61,6 +62,18 @@ proc.once('exit', common.mustCall(() => {
6162
return true;
6263
}));
6364

65+
// Promises are not AsyncWrap resources, their trace events are generated
66+
// from JavaScript as well.
67+
assert(traces.some((trace) => {
68+
if (trace.pid !== proc.pid)
69+
return false;
70+
if (trace.cat !== traceCategory('node.async_hooks'))
71+
return false;
72+
if (trace.name !== 'PROMISE')
73+
return false;
74+
return true;
75+
}));
76+
6477
// Check args in init events. Perfetto records the begin/end pair of an
6578
// async_hooks event as one complete event.
6679
const initPhase = common.hasPerfetto ? 'X' : 'b';

0 commit comments

Comments
 (0)