mirror of
https://github.com/nodejs/node.git
synced 2024-12-01 16:10:02 +01:00
7c079d1261
PR-URL: https://github.com/nodejs/node/pull/15454 Ref: https://github.com/nodejs/node/pull/14387 Ref: https://github.com/nodejs/node/pull/14722 Ref: https://github.com/nodejs/node/issues/14717 Ref: https://github.com/nodejs/node/issues/15448 Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
501 lines
17 KiB
JavaScript
501 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
const async_wrap = process.binding('async_wrap');
|
|
const errors = require('internal/errors');
|
|
/* async_hook_fields is a Uint32Array wrapping the uint32_t array of
|
|
* Environment::AsyncHooks::fields_[]. Each index tracks the number of active
|
|
* hooks for each type.
|
|
*
|
|
* async_id_fields is a Float64Array wrapping the double array of
|
|
* Environment::AsyncHooks::async_id_fields_[]. Each index contains the ids for
|
|
* the various asynchronous states of the application. These are:
|
|
* kExecutionAsyncId: The async_id assigned to the resource responsible for the
|
|
* current execution stack.
|
|
* kTriggerAsyncId: The trigger_async_id of the resource responsible for
|
|
* the current execution stack.
|
|
* kAsyncIdCounter: Incremental counter tracking the next assigned async_id.
|
|
* kInitTriggerAsyncId: Written immediately before a resource's constructor
|
|
* that sets the value of the init()'s triggerAsyncId. The order of
|
|
* retrieving the triggerAsyncId value is passing directly to the
|
|
* constructor -> value set in kInitTriggerAsyncId -> executionAsyncId of
|
|
* the current resource.
|
|
*/
|
|
const { async_hook_fields, async_id_fields } = async_wrap;
|
|
// Store the pair executionAsyncId and triggerAsyncId in a std::stack on
|
|
// Environment::AsyncHooks::ids_stack_ tracks the resource responsible for the
|
|
// current execution stack. This is unwound as each resource exits. In the case
|
|
// of a fatal exception this stack is emptied after calling each hook's after()
|
|
// callback.
|
|
const { pushAsyncIds, popAsyncIds } = async_wrap;
|
|
// For performance reasons, only track Proimses when a hook is enabled.
|
|
const { enablePromiseHook, disablePromiseHook } = async_wrap;
|
|
// Properties in active_hooks are used to keep track of the set of hooks being
|
|
// executed in case another hook is enabled/disabled. The new set of hooks is
|
|
// then restored once the active set of hooks is finished executing.
|
|
const active_hooks = {
|
|
// Array of all AsyncHooks that will be iterated whenever an async event
|
|
// fires. Using var instead of (preferably const) in order to assign
|
|
// active_hooks.tmp_array if a hook is enabled/disabled during hook
|
|
// execution.
|
|
array: [],
|
|
// Use a counter to track nested calls of async hook callbacks and make sure
|
|
// the active_hooks.array isn't altered mid execution.
|
|
call_depth: 0,
|
|
// Use to temporarily store and updated active_hooks.array if the user
|
|
// enables or disables a hook while hooks are being processed. If a hook is
|
|
// enabled() or disabled() during hook execution then the current set of
|
|
// active hooks is duplicated and set equal to active_hooks.tmp_array. Any
|
|
// subsequent changes are on the duplicated array. When all hooks have
|
|
// completed executing active_hooks.tmp_array is assigned to
|
|
// active_hooks.array.
|
|
tmp_array: null,
|
|
// Keep track of the field counts held in active_hooks.tmp_array. Because the
|
|
// async_hook_fields can't be reassigned, store each uint32 in an array that
|
|
// is written back to async_hook_fields when active_hooks.array is restored.
|
|
tmp_fields: null
|
|
};
|
|
|
|
|
|
// Each constant tracks how many callbacks there are for any given step of
|
|
// async execution. These are tracked so if the user didn't include callbacks
|
|
// for a given step, that step can bail out early.
|
|
const { kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,
|
|
kCheck, kExecutionAsyncId, kTriggerAsyncId, kAsyncIdCounter,
|
|
kInitTriggerAsyncId } = async_wrap.constants;
|
|
|
|
// Symbols used to store the respective ids on both AsyncResource instances and
|
|
// internal resources. They will also be assigned to arbitrary objects passed
|
|
// in by the user that take place of internally constructed objects.
|
|
const { async_id_symbol, trigger_async_id_symbol } = async_wrap;
|
|
|
|
// Used in AsyncHook and AsyncResource.
|
|
const init_symbol = Symbol('init');
|
|
const before_symbol = Symbol('before');
|
|
const after_symbol = Symbol('after');
|
|
const destroy_symbol = Symbol('destroy');
|
|
const promise_resolve_symbol = Symbol('promiseResolve');
|
|
const emitBeforeNative = emitHookFactory(before_symbol, 'emitBeforeNative');
|
|
const emitAfterNative = emitHookFactory(after_symbol, 'emitAfterNative');
|
|
const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative');
|
|
const emitPromiseResolveNative =
|
|
emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative');
|
|
|
|
// TODO(refack): move to node-config.cc
|
|
const abort_regex = /^--abort[_-]on[_-]uncaught[_-]exception$/;
|
|
|
|
// Setup the callbacks that node::AsyncWrap will call when there are hooks to
|
|
// process. They use the same functions as the JS embedder API. These callbacks
|
|
// are setup immediately to prevent async_wrap.setupHooks() from being hijacked
|
|
// and the cost of doing so is negligible.
|
|
async_wrap.setupHooks({ init: emitInitNative,
|
|
before: emitBeforeNative,
|
|
after: emitAfterNative,
|
|
destroy: emitDestroyNative,
|
|
promise_resolve: emitPromiseResolveNative });
|
|
|
|
// Used to fatally abort the process if a callback throws.
|
|
function fatalError(e) {
|
|
if (typeof e.stack === 'string') {
|
|
process._rawDebug(e.stack);
|
|
} else {
|
|
const o = { message: e };
|
|
Error.captureStackTrace(o, fatalError);
|
|
process._rawDebug(o.stack);
|
|
}
|
|
if (process.execArgv.some((e) => abort_regex.test(e))) {
|
|
process.abort();
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
|
|
// Public API //
|
|
|
|
class AsyncHook {
|
|
constructor({ init, before, after, destroy, promiseResolve }) {
|
|
if (init !== undefined && typeof init !== 'function')
|
|
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'init');
|
|
if (before !== undefined && typeof before !== 'function')
|
|
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before');
|
|
if (after !== undefined && typeof after !== 'function')
|
|
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before');
|
|
if (destroy !== undefined && typeof destroy !== 'function')
|
|
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before');
|
|
if (promiseResolve !== undefined && typeof promiseResolve !== 'function')
|
|
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'promiseResolve');
|
|
|
|
this[init_symbol] = init;
|
|
this[before_symbol] = before;
|
|
this[after_symbol] = after;
|
|
this[destroy_symbol] = destroy;
|
|
this[promise_resolve_symbol] = promiseResolve;
|
|
}
|
|
|
|
enable() {
|
|
// The set of callbacks for a hook should be the same regardless of whether
|
|
// enable()/disable() are run during their execution. The following
|
|
// references are reassigned to the tmp arrays if a hook is currently being
|
|
// processed.
|
|
const [hooks_array, hook_fields] = getHookArrays();
|
|
|
|
// Each hook is only allowed to be added once.
|
|
if (hooks_array.includes(this))
|
|
return this;
|
|
|
|
const prev_kTotals = hook_fields[kTotals];
|
|
hook_fields[kTotals] = 0;
|
|
|
|
// createHook() has already enforced that the callbacks are all functions,
|
|
// so here simply increment the count of whether each callbacks exists or
|
|
// not.
|
|
hook_fields[kTotals] += hook_fields[kInit] += +!!this[init_symbol];
|
|
hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
|
|
hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
|
|
hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
|
|
hook_fields[kTotals] +=
|
|
hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol];
|
|
hooks_array.push(this);
|
|
|
|
if (prev_kTotals === 0 && hook_fields[kTotals] > 0) {
|
|
enablePromiseHook();
|
|
hook_fields[kCheck] += 1;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
disable() {
|
|
const [hooks_array, hook_fields] = getHookArrays();
|
|
|
|
const index = hooks_array.indexOf(this);
|
|
if (index === -1)
|
|
return this;
|
|
|
|
const prev_kTotals = hook_fields[kTotals];
|
|
hook_fields[kTotals] = 0;
|
|
|
|
hook_fields[kTotals] += hook_fields[kInit] -= +!!this[init_symbol];
|
|
hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol];
|
|
hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol];
|
|
hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
|
|
hook_fields[kTotals] +=
|
|
hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol];
|
|
hooks_array.splice(index, 1);
|
|
|
|
if (prev_kTotals > 0 && hook_fields[kTotals] === 0) {
|
|
disablePromiseHook();
|
|
hook_fields[kCheck] -= 1;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
}
|
|
|
|
|
|
function getHookArrays() {
|
|
if (active_hooks.call_depth === 0)
|
|
return [active_hooks.array, async_hook_fields];
|
|
// If this hook is being enabled while in the middle of processing the array
|
|
// of currently active hooks then duplicate the current set of active hooks
|
|
// and store this there. This shouldn't fire until the next time hooks are
|
|
// processed.
|
|
if (active_hooks.tmp_array === null)
|
|
storeActiveHooks();
|
|
return [active_hooks.tmp_array, active_hooks.tmp_fields];
|
|
}
|
|
|
|
|
|
function storeActiveHooks() {
|
|
active_hooks.tmp_array = active_hooks.array.slice();
|
|
// Don't want to make the assumption that kInit to kDestroy are indexes 0 to
|
|
// 4. So do this the long way.
|
|
active_hooks.tmp_fields = [];
|
|
active_hooks.tmp_fields[kInit] = async_hook_fields[kInit];
|
|
active_hooks.tmp_fields[kBefore] = async_hook_fields[kBefore];
|
|
active_hooks.tmp_fields[kAfter] = async_hook_fields[kAfter];
|
|
active_hooks.tmp_fields[kDestroy] = async_hook_fields[kDestroy];
|
|
active_hooks.tmp_fields[kPromiseResolve] = async_hook_fields[kPromiseResolve];
|
|
}
|
|
|
|
|
|
// Then restore the correct hooks array in case any hooks were added/removed
|
|
// during hook callback execution.
|
|
function restoreActiveHooks() {
|
|
active_hooks.array = active_hooks.tmp_array;
|
|
async_hook_fields[kInit] = active_hooks.tmp_fields[kInit];
|
|
async_hook_fields[kBefore] = active_hooks.tmp_fields[kBefore];
|
|
async_hook_fields[kAfter] = active_hooks.tmp_fields[kAfter];
|
|
async_hook_fields[kDestroy] = active_hooks.tmp_fields[kDestroy];
|
|
async_hook_fields[kPromiseResolve] = active_hooks.tmp_fields[kPromiseResolve];
|
|
|
|
active_hooks.tmp_array = null;
|
|
active_hooks.tmp_fields = null;
|
|
}
|
|
|
|
|
|
function createHook(fns) {
|
|
return new AsyncHook(fns);
|
|
}
|
|
|
|
|
|
function executionAsyncId() {
|
|
return async_id_fields[kExecutionAsyncId];
|
|
}
|
|
|
|
|
|
function triggerAsyncId() {
|
|
return async_id_fields[kTriggerAsyncId];
|
|
}
|
|
|
|
function validateAsyncId(asyncId, type) {
|
|
// Skip validation when async_hooks is disabled
|
|
if (async_hook_fields[kCheck] <= 0) return;
|
|
|
|
if (!Number.isSafeInteger(asyncId) || asyncId < -1) {
|
|
fatalError(new errors.RangeError('ERR_INVALID_ASYNC_ID', type, asyncId));
|
|
}
|
|
}
|
|
|
|
|
|
// Embedder API //
|
|
|
|
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) {
|
|
throw new errors.RangeError('ERR_INVALID_ASYNC_ID',
|
|
'triggerAsyncId',
|
|
triggerAsyncId);
|
|
}
|
|
|
|
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
|
|
this[trigger_async_id_symbol] = triggerAsyncId;
|
|
|
|
emitInitScript(
|
|
this[async_id_symbol], type, this[trigger_async_id_symbol], this
|
|
);
|
|
}
|
|
|
|
emitBefore() {
|
|
emitBeforeScript(this[async_id_symbol], this[trigger_async_id_symbol]);
|
|
return this;
|
|
}
|
|
|
|
emitAfter() {
|
|
emitAfterScript(this[async_id_symbol]);
|
|
return this;
|
|
}
|
|
|
|
emitDestroy() {
|
|
emitDestroyScript(this[async_id_symbol]);
|
|
return this;
|
|
}
|
|
|
|
asyncId() {
|
|
return this[async_id_symbol];
|
|
}
|
|
|
|
triggerAsyncId() {
|
|
return this[trigger_async_id_symbol];
|
|
}
|
|
}
|
|
|
|
|
|
function runInAsyncIdScope(asyncId, cb) {
|
|
// Store the async id now to make sure the stack is still good when the ids
|
|
// are popped off the stack.
|
|
const prevId = executionAsyncId();
|
|
pushAsyncIds(asyncId, prevId);
|
|
try {
|
|
cb();
|
|
} finally {
|
|
popAsyncIds(asyncId);
|
|
}
|
|
}
|
|
|
|
|
|
// Sensitive Embedder API //
|
|
|
|
// Increment the internal id counter and return the value. Important that the
|
|
// counter increment first. Since it's done the same way in
|
|
// Environment::new_async_uid()
|
|
function newUid() {
|
|
return ++async_id_fields[kAsyncIdCounter];
|
|
}
|
|
|
|
|
|
// Return the triggerAsyncId meant for the constructor calling it. It's up to
|
|
// the user to safeguard this call and make sure it's zero'd out when the
|
|
// constructor is complete.
|
|
function initTriggerId() {
|
|
var triggerAsyncId = async_id_fields[kInitTriggerAsyncId];
|
|
// Reset value after it's been called so the next constructor doesn't
|
|
// inherit it by accident.
|
|
async_id_fields[kInitTriggerAsyncId] = 0;
|
|
if (triggerAsyncId <= 0)
|
|
triggerAsyncId = async_id_fields[kExecutionAsyncId];
|
|
return triggerAsyncId;
|
|
}
|
|
|
|
|
|
function setInitTriggerId(triggerAsyncId) {
|
|
// CHECK(Number.isSafeInteger(triggerAsyncId))
|
|
// CHECK(triggerAsyncId > 0)
|
|
async_id_fields[kInitTriggerAsyncId] = triggerAsyncId;
|
|
}
|
|
|
|
|
|
function emitInitScript(asyncId, type, triggerAsyncId, resource) {
|
|
validateAsyncId(asyncId, 'asyncId');
|
|
if (triggerAsyncId !== null)
|
|
validateAsyncId(triggerAsyncId, 'triggerAsyncId');
|
|
if (async_hook_fields[kCheck] > 0 &&
|
|
(typeof type !== 'string' || type.length <= 0)) {
|
|
throw new errors.TypeError('ERR_ASYNC_TYPE', type);
|
|
}
|
|
|
|
// Short circuit all checks for the common case. Which is that no hooks have
|
|
// been set. Do this to remove performance impact for embedders (and core).
|
|
if (async_hook_fields[kInit] === 0)
|
|
return;
|
|
|
|
// This can run after the early return check b/c running this function
|
|
// manually means that the embedder must have used initTriggerId().
|
|
if (triggerAsyncId === null) {
|
|
triggerAsyncId = initTriggerId();
|
|
} else {
|
|
// If a triggerAsyncId was passed, any kInitTriggerAsyncId still must be
|
|
// null'd.
|
|
async_id_fields[kInitTriggerAsyncId] = 0;
|
|
}
|
|
|
|
emitInitNative(asyncId, type, triggerAsyncId, resource);
|
|
}
|
|
|
|
function emitHookFactory(symbol, name) {
|
|
// Called from native. The asyncId stack handling is taken care of there
|
|
// before this is called.
|
|
// eslint-disable-next-line func-style
|
|
const fn = function(asyncId) {
|
|
active_hooks.call_depth += 1;
|
|
// Use a single try/catch for all hook to avoid setting up one per
|
|
// iteration.
|
|
try {
|
|
for (var i = 0; i < active_hooks.array.length; i++) {
|
|
if (typeof active_hooks.array[i][symbol] === 'function') {
|
|
active_hooks.array[i][symbol](asyncId);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
fatalError(e);
|
|
} finally {
|
|
active_hooks.call_depth -= 1;
|
|
}
|
|
|
|
// Hooks can only be restored if there have been no recursive hook calls.
|
|
// Also the active hooks do not need to be restored if enable()/disable()
|
|
// weren't called during hook execution, in which case
|
|
// active_hooks.tmp_array will be null.
|
|
if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) {
|
|
restoreActiveHooks();
|
|
}
|
|
};
|
|
|
|
// Set the name property of the anonymous function as it looks good in the
|
|
// stack trace.
|
|
Object.defineProperty(fn, 'name', {
|
|
value: name
|
|
});
|
|
return fn;
|
|
}
|
|
|
|
|
|
function emitBeforeScript(asyncId, triggerAsyncId) {
|
|
// Validate the ids. An id of -1 means it was never set and is visible on the
|
|
// call graph. An id < -1 should never happen in any circumstance. Throw
|
|
// on user calls because async state should still be recoverable.
|
|
validateAsyncId(asyncId, 'asyncId');
|
|
validateAsyncId(triggerAsyncId, 'triggerAsyncId');
|
|
|
|
pushAsyncIds(asyncId, triggerAsyncId);
|
|
|
|
if (async_hook_fields[kBefore] > 0)
|
|
emitBeforeNative(asyncId);
|
|
}
|
|
|
|
|
|
function emitAfterScript(asyncId) {
|
|
validateAsyncId(asyncId, 'asyncId');
|
|
|
|
if (async_hook_fields[kAfter] > 0)
|
|
emitAfterNative(asyncId);
|
|
|
|
popAsyncIds(asyncId);
|
|
}
|
|
|
|
|
|
function emitDestroyScript(asyncId) {
|
|
validateAsyncId(asyncId, 'asyncId');
|
|
|
|
// Return early if there are no destroy callbacks, or invalid asyncId.
|
|
if (async_hook_fields[kDestroy] === 0 || asyncId <= 0)
|
|
return;
|
|
async_wrap.queueDestroyAsyncId(asyncId);
|
|
}
|
|
|
|
|
|
// Used by C++ to call all init() callbacks. Because some state can be setup
|
|
// from C++ there's no need to perform all the same operations as in
|
|
// emitInitScript.
|
|
function emitInitNative(asyncId, type, triggerAsyncId, resource) {
|
|
active_hooks.call_depth += 1;
|
|
// Use a single try/catch for all hook to avoid setting up one per iteration.
|
|
try {
|
|
for (var i = 0; i < active_hooks.array.length; i++) {
|
|
if (typeof active_hooks.array[i][init_symbol] === 'function') {
|
|
active_hooks.array[i][init_symbol](
|
|
asyncId, type, triggerAsyncId,
|
|
resource
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
fatalError(e);
|
|
} finally {
|
|
active_hooks.call_depth -= 1;
|
|
}
|
|
|
|
// Hooks can only be restored if there have been no recursive hook calls.
|
|
// Also the active hooks do not need to be restored if enable()/disable()
|
|
// weren't called during hook execution, in which case active_hooks.tmp_array
|
|
// will be null.
|
|
if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) {
|
|
restoreActiveHooks();
|
|
}
|
|
}
|
|
|
|
|
|
// Placing all exports down here because the exported classes won't export
|
|
// otherwise.
|
|
module.exports = {
|
|
// Public API
|
|
createHook,
|
|
executionAsyncId,
|
|
triggerAsyncId,
|
|
// Embedder API
|
|
AsyncResource,
|
|
runInAsyncIdScope,
|
|
// Sensitive Embedder API
|
|
newUid,
|
|
initTriggerId,
|
|
setInitTriggerId,
|
|
emitInit: emitInitScript,
|
|
emitBefore: emitBeforeScript,
|
|
emitAfter: emitAfterScript,
|
|
emitDestroy: emitDestroyScript,
|
|
};
|