From e066706b3ca1a4142c2533c2eba5e81e8d40dd8a Mon Sep 17 00:00:00 2001 From: Andreas Madsen Date: Sun, 12 Nov 2017 18:46:55 +0100 Subject: [PATCH] async_hooks: deprecate undocumented API PR-URL: https://github.com/nodejs/node/pull/16972 Backport-PR-URL: https://github.com/nodejs/node/pull/18179 Refs: https://github.com/nodejs/node/issues/14328 Refs: https://github.com/nodejs/node/issues/15572 Reviewed-By: Anna Henningsen --- doc/api/deprecations.md | 19 + lib/async_hooks.js | 430 ++++-------------- lib/dgram.js | 2 +- lib/internal/async_hooks.js | 349 ++++++++++++++ lib/internal/bootstrap_node.js | 2 +- lib/internal/process/next_tick.js | 2 +- lib/net.js | 2 +- lib/timers.js | 2 +- node.gyp | 1 + test/async-hooks/test-callback-error.js | 21 +- test/async-hooks/test-emit-before-after.js | 11 +- test/async-hooks/test-emit-init.js | 13 +- .../test-async-hooks-run-in-async-id-scope.js | 2 +- .../test-http-client-immediate-error.js | 3 +- 14 files changed, 496 insertions(+), 363 deletions(-) create mode 100644 lib/internal/async_hooks.js diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 3652f469c83fef..aaa31dd6781825 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -665,6 +665,25 @@ function for [`util.inspect()`][] is deprecated. Use [`util.inspect.custom`][] instead. For backwards compatibility with Node.js prior to version 6.4.0, both may be specified. + +### DEP0085: AsyncHooks Sensitive API + +Type: Runtime + +The AsyncHooks Sensitive API was never documented and had various of minor +issues, see https://github.com/nodejs/node/issues/15572. Use the `AsyncResource` +API instead. + + + +### DEP0086: Remove runInAsyncIdScope + +Type: Runtime + +`runInAsyncIdScope` doesn't emit the `before` or `after` event and can thus +cause a lot of issues. See https://github.com/nodejs/node/issues/14328 for more +details. + [`Buffer.allocUnsafeSlow(size)`]: buffer.html#buffer_class_method_buffer_allocunsafeslow_size [`Buffer.from(array)`]: buffer.html#buffer_class_method_buffer_from_array [`Buffer.from(buffer)`]: buffer.html#buffer_class_method_buffer_from_buffer diff --git a/lib/async_hooks.js b/lib/async_hooks.js index b642ec1a82f094..52c887ecb1b229 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -1,119 +1,50 @@ 'use strict'; +const errors = require('internal/errors'); const internalUtil = require('internal/util'); 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 internal_async_hooks = require('internal/async_hooks'); + +// Get functions +// Only used to support a deprecated API. pushAsyncIds, popAsyncIds should +// never be directly in this manner. const { pushAsyncIds, popAsyncIds } = async_wrap; -// For performance reasons, only track Proimses when a hook is enabled. -const { enablePromiseHook, disablePromiseHook } = async_wrap; // For userland AsyncResources, make sure to emit a destroy event when the // resource gets gced. const { registerDestroyHook } = 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 -}; +const { + // Private API + getHookArrays, + enableHooks, + disableHooks, + // Sensitive Embedder API + newUid, + initTriggerId, + setInitTriggerId, + emitInit, + emitBefore, + emitAfter, + emitDestroy, +} = internal_async_hooks; +// Get fields +const { async_id_fields } = async_wrap; -// 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; +// Get symbols +const { + init_symbol, before_symbol, after_symbol, destroy_symbol, + promise_resolve_symbol +} = internal_async_hooks.symbols; -// 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); -} - +// Get constants +const { + kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve, + kExecutionAsyncId, kTriggerAsyncId +} = async_wrap.constants; -// Public API // +// Listener API // class AsyncHook { constructor({ init, before, after, destroy, promiseResolve }) { @@ -161,8 +92,7 @@ class AsyncHook { hooks_array.push(this); if (prev_kTotals === 0 && hook_fields[kTotals] > 0) { - enablePromiseHook(); - hook_fields[kCheck] += 1; + enableHooks(); } return this; @@ -187,8 +117,7 @@ class AsyncHook { hooks_array.splice(index, 1); if (prev_kTotals > 0 && hook_fields[kTotals] === 0) { - disablePromiseHook(); - hook_fields[kCheck] -= 1; + disableHooks(); } return this; @@ -196,47 +125,6 @@ class AsyncHook { } -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); } @@ -251,15 +139,6 @@ 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 // @@ -285,12 +164,12 @@ class AsyncResource { triggerAsyncId); } - this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter]; + this[async_id_symbol] = newUid(); this[trigger_async_id_symbol] = triggerAsyncId; // this prop name (destroyed) has to be synchronized with C++ this[destroyedSymbol] = { destroyed: false }; - emitInitScript( + emitInit( this[async_id_symbol], type, this[trigger_async_id_symbol], this ); @@ -300,18 +179,18 @@ class AsyncResource { } emitBefore() { - emitBeforeScript(this[async_id_symbol], this[trigger_async_id_symbol]); + emitBefore(this[async_id_symbol], this[trigger_async_id_symbol]); return this; } emitAfter() { - emitAfterScript(this[async_id_symbol]); + emitAfter(this[async_id_symbol]); return this; } emitDestroy() { this[destroyedSymbol].destroyed = true; - emitDestroyScript(this[async_id_symbol]); + emitDestroy(this[async_id_symbol]); return this; } @@ -348,168 +227,6 @@ function runInAsyncIdScope(asyncId, cb) { } } - -// 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 = { @@ -519,17 +236,10 @@ module.exports = { triggerAsyncId, // Embedder API AsyncResource, - runInAsyncIdScope, - // Sensitive Embedder API - newUid, - initTriggerId, - setInitTriggerId, - emitInit: emitInitScript, - emitBefore: emitBeforeScript, - emitAfter: emitAfterScript, - emitDestroy: emitDestroyScript, }; +// Deprecated API // + // currentId was renamed to executionAsyncId. This was in 8.2.0 during the // experimental stage so the alias can be removed at any time, we are just // being nice :) @@ -549,3 +259,59 @@ Object.defineProperty(module.exports, 'triggerId', { }, 'async_hooks.triggerId is deprecated. ' + 'Use async_hooks.triggerAsyncId instead.', 'DEP0071') }); + +Object.defineProperty(module.exports, 'runInAsyncIdScope', { + get: internalUtil.deprecate(function() { + return runInAsyncIdScope; + }, 'async_hooks.runInAsyncIdScope is deprecated. ' + + 'Create an AsyncResource instead.', 'DEP0086') +}); + +Object.defineProperty(module.exports, 'newUid', { + get: internalUtil.deprecate(function() { + return newUid; + }, 'async_hooks.newUid is deprecated. ' + + 'Use AsyncResource instead.', 'DEP0085') +}); + +Object.defineProperty(module.exports, 'initTriggerId', { + get: internalUtil.deprecate(function() { + return initTriggerId; + }, 'async_hooks.initTriggerId is deprecated. ' + + 'Use the AsyncResource default instead.', 'DEP0085') +}); + +Object.defineProperty(module.exports, 'setInitTriggerId', { + get: internalUtil.deprecate(function() { + return setInitTriggerId; + }, 'async_hooks.setInitTriggerId is deprecated. ' + + 'Use the triggerAsyncId parameter in AsyncResource instead.', 'DEP0085') +}); + +Object.defineProperty(module.exports, 'emitInit', { + get: internalUtil.deprecate(function() { + return emitInit; + }, 'async_hooks.emitInit is deprecated. ' + + 'Use AsyncResource constructor instead.', 'DEP0085') +}); + +Object.defineProperty(module.exports, 'emitBefore', { + get: internalUtil.deprecate(function() { + return emitBefore; + }, 'async_hooks.emitBefore is deprecated. ' + + 'Use AsyncResource.emitBefore instead.', 'DEP0085') +}); + +Object.defineProperty(module.exports, 'emitAfter', { + get: internalUtil.deprecate(function() { + return emitAfter; + }, 'async_hooks.emitAfter is deprecated. ' + + 'Use AsyncResource.emitAfter instead.', 'DEP0085') +}); + +Object.defineProperty(module.exports, 'emitDestroy', { + get: internalUtil.deprecate(function() { + return emitDestroy; + }, 'async_hooks.emitDestroy is deprecated. ' + + 'Use AsyncResource.emitDestroy instead.', 'DEP0085') +}); diff --git a/lib/dgram.js b/lib/dgram.js index ef07a4f85bdeed..8fe52713bc37aa 100644 --- a/lib/dgram.js +++ b/lib/dgram.js @@ -28,7 +28,7 @@ const dns = require('dns'); const util = require('util'); const { isUint8Array } = require('internal/util/types'); const EventEmitter = require('events'); -const { setInitTriggerId } = require('async_hooks'); +const { setInitTriggerId } = require('internal/async_hooks'); const { UV_UDP_REUSEADDR } = process.binding('constants').os; const { async_id_symbol } = process.binding('async_wrap'); const { nextTick } = require('internal/process/next_tick'); diff --git a/lib/internal/async_hooks.js b/lib/internal/async_hooks.js new file mode 100644 index 00000000000000..5964a847fc0a25 --- /dev/null +++ b/lib/internal/async_hooks.js @@ -0,0 +1,349 @@ +'use strict'; + +const errors = require('internal/errors'); +const async_wrap = process.binding('async_wrap'); +/* 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, kPromiseResolve, + kCheck, kExecutionAsyncId, kAsyncIdCounter, + kInitTriggerAsyncId } = async_wrap.constants; + +// 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); +} + + +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)); + } +} + +// Emit From Native // + +// 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(); + } +} + + +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; +} + +// Manage Active Hooks // + +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 enableHooks() { + enablePromiseHook(); + async_hook_fields[kCheck] += 1; +} + +function disableHooks() { + disablePromiseHook(); + async_hook_fields[kCheck] -= 1; +} + +// 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 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); +} + + +module.exports = { + // Private API + getHookArrays, + symbols: { + init_symbol, before_symbol, after_symbol, destroy_symbol, + promise_resolve_symbol + }, + enableHooks, + disableHooks, + // Sensitive Embedder API + newUid, + initTriggerId, + setInitTriggerId, + emitInit: emitInitScript, + emitBefore: emitBeforeScript, + emitAfter: emitAfterScript, + emitDestroy: emitDestroyScript, +}; diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index bebf47e5837860..f751cf45e5878e 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -393,7 +393,7 @@ // Emit the after() hooks now that the exception has been handled. if (async_hook_fields[kAfter] > 0) { do { - NativeModule.require('async_hooks').emitAfter( + NativeModule.require('internal/async_hooks').emitAfter( async_id_fields[kExecutionAsyncId]); } while (asyncIdStackSize() > 0); // Or completely empty the id stack. diff --git a/lib/internal/process/next_tick.js b/lib/internal/process/next_tick.js index fa144c5969b6f9..59a1e4fee1c75f 100644 --- a/lib/internal/process/next_tick.js +++ b/lib/internal/process/next_tick.js @@ -48,7 +48,7 @@ class NextTickQueue { function setupNextTick() { const async_wrap = process.binding('async_wrap'); - const async_hooks = require('async_hooks'); + const async_hooks = require('internal/async_hooks'); const promises = require('internal/process/promises'); const errors = require('internal/errors'); const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks); diff --git a/lib/net.js b/lib/net.js index 4d373bc1e10c33..43ce4d803e03af 100644 --- a/lib/net.js +++ b/lib/net.js @@ -39,7 +39,7 @@ const { TCPConnectWrap } = process.binding('tcp_wrap'); const { PipeConnectWrap } = process.binding('pipe_wrap'); const { ShutdownWrap, WriteWrap } = process.binding('stream_wrap'); const { async_id_symbol } = process.binding('async_wrap'); -const { newUid, setInitTriggerId } = require('async_hooks'); +const { newUid, setInitTriggerId } = require('internal/async_hooks'); const { nextTick } = require('internal/process/next_tick'); const errors = require('internal/errors'); const dns = require('dns'); diff --git a/lib/timers.js b/lib/timers.js index c7237a25353e5f..d12c080cf7ab5d 100644 --- a/lib/timers.js +++ b/lib/timers.js @@ -39,7 +39,7 @@ const { emitBefore, emitAfter, emitDestroy -} = require('async_hooks'); +} = require('internal/async_hooks'); // Grab the constants necessary for working with internal arrays. const { kInit, kDestroy, kAsyncIdCounter } = async_wrap.constants; // Symbols for storing async id state. diff --git a/node.gyp b/node.gyp index 4ec7da6c2f1126..8678093d7442ad 100644 --- a/node.gyp +++ b/node.gyp @@ -77,6 +77,7 @@ 'lib/v8.js', 'lib/vm.js', 'lib/zlib.js', + 'lib/internal/async_hooks.js', 'lib/internal/buffer.js', 'lib/internal/child_process.js', 'lib/internal/cluster/child.js', diff --git a/test/async-hooks/test-callback-error.js b/test/async-hooks/test-callback-error.js index a4b8a99f33e858..09eb2e0b478a6e 100644 --- a/test/async-hooks/test-callback-error.js +++ b/test/async-hooks/test-callback-error.js @@ -11,35 +11,22 @@ switch (arg) { initHooks({ oninit: common.mustCall(() => { throw new Error(arg); }) }).enable(); - async_hooks.emitInit( - async_hooks.newUid(), - `${arg}_type`, - async_hooks.executionAsyncId() - ); + new async_hooks.AsyncResource(`${arg}_type`); return; case 'test_callback': initHooks({ onbefore: common.mustCall(() => { throw new Error(arg); }) }).enable(); - const newAsyncId = async_hooks.newUid(); - async_hooks.emitInit( - newAsyncId, - `${arg}_type`, - async_hooks.executionAsyncId() - ); - async_hooks.emitBefore(newAsyncId, async_hooks.executionAsyncId()); + const resource = new async_hooks.AsyncResource(`${arg}_type`); + resource.emitBefore(); return; case 'test_callback_abort': initHooks({ oninit: common.mustCall(() => { throw new Error(arg); }) }).enable(); - async_hooks.emitInit( - async_hooks.newUid(), - `${arg}_type`, - async_hooks.executionAsyncId() - ); + new async_hooks.AsyncResource(`${arg}_type`); return; } diff --git a/test/async-hooks/test-emit-before-after.js b/test/async-hooks/test-emit-before-after.js index 2b22739fa9478d..1b28c1e42622dd 100644 --- a/test/async-hooks/test-emit-before-after.js +++ b/test/async-hooks/test-emit-before-after.js @@ -1,9 +1,10 @@ 'use strict'; +// Flags: --expose-internals const common = require('../common'); const assert = require('assert'); const spawnSync = require('child_process').spawnSync; -const async_hooks = require('async_hooks'); +const async_hooks = require('internal/async_hooks'); const initHooks = require('./init-hooks'); switch (process.argv[2]) { @@ -17,13 +18,17 @@ switch (process.argv[2]) { assert.ok(!process.argv[2]); -const c1 = spawnSync(process.execPath, [__filename, 'test_invalid_async_id']); +const c1 = spawnSync(process.execPath, [ + '--expose-internals', __filename, 'test_invalid_async_id' +]); assert.strictEqual( c1.stderr.toString().split(/[\r\n]+/g)[0], 'RangeError [ERR_INVALID_ASYNC_ID]: Invalid asyncId value: -2'); assert.strictEqual(c1.status, 1); -const c2 = spawnSync(process.execPath, [__filename, 'test_invalid_trigger_id']); +const c2 = spawnSync(process.execPath, [ + '--expose-internals', __filename, 'test_invalid_trigger_id' +]); assert.strictEqual( c2.stderr.toString().split(/[\r\n]+/g)[0], 'RangeError [ERR_INVALID_ASYNC_ID]: Invalid triggerAsyncId value: -2'); diff --git a/test/async-hooks/test-emit-init.js b/test/async-hooks/test-emit-init.js index 9c61f19dab7784..e69285d4f81c84 100644 --- a/test/async-hooks/test-emit-init.js +++ b/test/async-hooks/test-emit-init.js @@ -1,9 +1,10 @@ 'use strict'; +// Flags: --expose-internals const common = require('../common'); const assert = require('assert'); const spawnSync = require('child_process').spawnSync; -const async_hooks = require('async_hooks'); +const async_hooks = require('internal/async_hooks'); const initHooks = require('./init-hooks'); const expectedId = async_hooks.newUid(); @@ -36,20 +37,24 @@ switch (process.argv[2]) { assert.ok(!process.argv[2]); -const c1 = spawnSync(process.execPath, [__filename, 'test_invalid_async_id']); +const c1 = spawnSync(process.execPath, [ + '--expose-internals', __filename, 'test_invalid_async_id' +]); assert.strictEqual( c1.stderr.toString().split(/[\r\n]+/g)[0], 'RangeError [ERR_INVALID_ASYNC_ID]: Invalid asyncId value: undefined'); assert.strictEqual(c1.status, 1); -const c2 = spawnSync(process.execPath, [__filename, 'test_invalid_trigger_id']); +const c2 = spawnSync(process.execPath, [ + '--expose-internals', __filename, 'test_invalid_trigger_id' +]); assert.strictEqual( c2.stderr.toString().split(/[\r\n]+/g)[0], 'RangeError [ERR_INVALID_ASYNC_ID]: Invalid triggerAsyncId value: undefined'); assert.strictEqual(c2.status, 1); const c3 = spawnSync(process.execPath, [ - __filename, 'test_invalid_trigger_id_negative' + '--expose-internals', __filename, 'test_invalid_trigger_id_negative' ]); assert.strictEqual( c3.stderr.toString().split(/[\r\n]+/g)[0], diff --git a/test/parallel/test-async-hooks-run-in-async-id-scope.js b/test/parallel/test-async-hooks-run-in-async-id-scope.js index 8cef7d214c2b4f..14d1c7423fcf29 100644 --- a/test/parallel/test-async-hooks-run-in-async-id-scope.js +++ b/test/parallel/test-async-hooks-run-in-async-id-scope.js @@ -4,7 +4,7 @@ const common = require('../common'); const assert = require('assert'); const async_hooks = require('async_hooks'); -const asyncId = async_hooks.newUid(); +const asyncId = new async_hooks.AsyncResource('test').asyncId(); assert.notStrictEqual(async_hooks.executionAsyncId(), asyncId); diff --git a/test/parallel/test-http-client-immediate-error.js b/test/parallel/test-http-client-immediate-error.js index 6b9cacb256f927..abbf5c41fc6660 100644 --- a/test/parallel/test-http-client-immediate-error.js +++ b/test/parallel/test-http-client-immediate-error.js @@ -1,4 +1,5 @@ 'use strict'; +// Flags: --expose-internals // Make sure http.request() can catch immediate errors in // net.createConnection(). @@ -9,7 +10,7 @@ const net = require('net'); const http = require('http'); const uv = process.binding('uv'); const { async_id_symbol } = process.binding('async_wrap'); -const { newUid } = require('async_hooks'); +const { newUid } = require('internal/async_hooks'); const agent = new http.Agent(); agent.createConnection = common.mustCall((cfg) => {