diff options
author | Trevor Norris <trev.norris@gmail.com> | 2017-03-09 16:13:34 -0700 |
---|---|---|
committer | Anna Henningsen <anna@addaleax.net> | 2017-05-10 22:22:26 +0200 |
commit | 7e3a3c962f09233c53cee7ebe381341d7c8b7162 (patch) | |
tree | 02963f7724ed9099a3566f13b16619fddffe0793 /lib/async_hooks.js | |
parent | c0bde73f1bbfedd4e77ddf87cf0bcec7bac9a61e (diff) | |
download | node-new-7e3a3c962f09233c53cee7ebe381341d7c8b7162.tar.gz |
async_hooks: initial async_hooks implementation
Fill this commit messsage with more details about the change once all
changes are rebased.
* Add lib/async_hooks.js
* Add JS methods to AsyncWrap for handling the async id stack
* Introduce AsyncReset() so that JS functions can reset the id and again
trigger the init hooks, allow AsyncWrap::Reset() to be called from JS
via asyncReset().
* Add env variable to test additional things in test/common.js
PR-URL: https://github.com/nodejs/node/pull/12892
Ref: https://github.com/nodejs/node/pull/11883
Ref: https://github.com/nodejs/node/pull/8531
Reviewed-By: Andreas Madsen <amwebdk@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
Diffstat (limited to 'lib/async_hooks.js')
-rw-r--r-- | lib/async_hooks.js | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/lib/async_hooks.js b/lib/async_hooks.js new file mode 100644 index 0000000000..736b189097 --- /dev/null +++ b/lib/async_hooks.js @@ -0,0 +1,488 @@ +'use strict'; + +const async_wrap = process.binding('async_wrap'); +/* Both these arrays are used to communicate between JS and C++ with as little + * overhead as possible. + * + * async_hook_fields is a Uint32Array() that communicates the number of each + * type of active hooks of each type and wraps the uin32_t array of + * node::Environment::AsyncHooks::fields_. + * + * async_uid_fields is a Float64Array() that contains the async/trigger ids for + * several operations. These fields are as follows: + * kCurrentAsyncId: The async id of the current execution stack. + * kCurrentTriggerId: The trigger id of the current execution stack. + * kAsyncUidCntr: Counter that tracks the unique ids given to new resources. + * kInitTriggerId: Written to just before creating a new resource, so the + * constructor knows what other resource is responsible for its init(). + * Used this way so the trigger id doesn't need to be passed to every + * resource's constructor. + */ +const { async_hook_fields, async_uid_fields } = async_wrap; +// Used to change the state of the async id stack. +const { pushAsyncIds, popAsyncIds } = async_wrap; +// Array of all AsyncHooks that will be iterated whenever an async event fires. +// Using var instead of (preferably const) in order to assign +// tmp_active_hooks_array if a hook is enabled/disabled during hook execution. +var active_hooks_array = []; +// Track whether a hook callback is currently being processed. Used to make +// sure active_hooks_array isn't altered in mid execution if another hook is +// added or removed. +var processing_hook = false; +// Use to temporarily store and updated active_hooks_array if the user enables +// or disables a hook while hooks are being processed. +var tmp_active_hooks_array = null; +// Keep track of the field counds held in tmp_active_hooks_array. +var tmp_async_hook_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, kCurrentAsyncId, kCurrentTriggerId, + kAsyncUidCntr, kInitTriggerId } = async_wrap.constants; + +// Used in AsyncHook and AsyncEvent. +const async_id_symbol = Symbol('_asyncId'); +const trigger_id_symbol = Symbol('_triggerId'); +const init_symbol = Symbol('init'); +const before_symbol = Symbol('before'); +const after_symbol = Symbol('after'); +const destroy_symbol = Symbol('destroy'); + +// Setup the callbacks that node::AsyncWrap will call when there are hooks to +// process. They use the same functions as the JS embedder API. +async_wrap.setupHooks({ init, + before: emitBeforeN, + after: emitAfterN, + destroy: emitDestroyN }); + +// 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[_-]on[_-]uncaught[_-]exception$/.test(e))) { + process.abort(); + } + process.exit(1); +} + + +// Public API // + +class AsyncHook { + constructor({ init, before, after, destroy }) { + if (init && typeof init !== 'function') + throw new TypeError('init must be a function'); + if (before && typeof before !== 'function') + throw new TypeError('before must be a function'); + if (after && typeof after !== 'function') + throw new TypeError('after must be a function'); + if (destroy && typeof destroy !== 'function') + throw new TypeError('destroy must be a function'); + + this[init_symbol] = init; + this[before_symbol] = before; + this[after_symbol] = after; + this[destroy_symbol] = destroy; + } + + 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; + + // 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[kInit] += +!!this[init_symbol]; + hook_fields[kBefore] += +!!this[before_symbol]; + hook_fields[kAfter] += +!!this[after_symbol]; + hook_fields[kDestroy] += +!!this[destroy_symbol]; + hooks_array.push(this); + return this; + } + + disable() { + const [hooks_array, hook_fields] = getHookArrays(); + + const index = hooks_array.indexOf(this); + if (index === -1) + return; + + hook_fields[kInit] -= +!!this[init_symbol]; + hook_fields[kBefore] -= +!!this[before_symbol]; + hook_fields[kAfter] -= +!!this[after_symbol]; + hook_fields[kDestroy] -= +!!this[destroy_symbol]; + hooks_array.splice(index, 1); + return this; + } +} + + +function getHookArrays() { + if (!processing_hook) + 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 (tmp_active_hooks_array === null) + storeActiveHooks(); + return [tmp_active_hooks_array, tmp_async_hook_fields]; +} + + +function storeActiveHooks() { + tmp_active_hooks_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. + tmp_async_hook_fields = []; + tmp_async_hook_fields[kInit] = async_hook_fields[kInit]; + tmp_async_hook_fields[kBefore] = async_hook_fields[kBefore]; + tmp_async_hook_fields[kAfter] = async_hook_fields[kAfter]; + tmp_async_hook_fields[kDestroy] = async_hook_fields[kDestroy]; +} + + +// Then restore the correct hooks array in case any hooks were added/removed +// during hook callback execution. +function restoreTmpHooks() { + active_hooks_array = tmp_active_hooks_array; + async_hook_fields[kInit] = tmp_async_hook_fields[kInit]; + async_hook_fields[kBefore] = tmp_async_hook_fields[kBefore]; + async_hook_fields[kAfter] = tmp_async_hook_fields[kAfter]; + async_hook_fields[kDestroy] = tmp_async_hook_fields[kDestroy]; + + tmp_active_hooks_array = null; + tmp_async_hook_fields = null; +} + + +function createHook(fns) { + return new AsyncHook(fns); +} + + +function currentId() { + return async_uid_fields[kCurrentAsyncId]; +} + + +function triggerId() { + return async_uid_fields[kCurrentTriggerId]; +} + + +// Embedder API // + +class AsyncEvent { + constructor(type, triggerId) { + this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + // Read and reset the current kInitTriggerId so that when the constructor + // finishes the kInitTriggerId field is always 0. + if (triggerId === undefined) { + triggerId = initTriggerId(); + // If a triggerId was passed, any kInitTriggerId still must be null'd. + } else { + async_uid_fields[kInitTriggerId] = 0; + } + this[trigger_id_symbol] = triggerId; + + // Return immediately if there's nothing to do. + if (async_hook_fields[kInit] === 0) + return; + + if (typeof type !== 'string' || type.length <= 0) + throw new TypeError('type must be a string with length > 0'); + if (!Number.isSafeInteger(triggerId) || triggerId < 0) + throw new RangeError('triggerId must be an unsigned integer'); + + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][init_symbol] === 'function') { + runInitCallback(active_hooks_array[i][init_symbol], + this[async_id_symbol], + type, + triggerId, + this); + } + } + processing_hook = false; + } + + emitBefore() { + emitBeforeS(this[async_id_symbol], this[trigger_id_symbol]); + return this; + } + + emitAfter() { + emitAfterS(this[async_id_symbol]); + return this; + } + + emitDestroy() { + emitDestroyS(this[async_id_symbol]); + return this; + } + + asyncId() { + return this[async_id_symbol]; + } + + triggerId() { + return this[trigger_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 = currentId(); + 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_uid_fields[kAsyncUidCntr]; +} + + +// Return the triggerId 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 tId = async_uid_fields[kInitTriggerId]; + // Reset value after it's been called so the next constructor doesn't + // inherit it by accident. + async_uid_fields[kInitTriggerId] = 0; + if (tId <= 0) + tId = async_uid_fields[kCurrentAsyncId]; + return tId; +} + + +function setInitTriggerId(triggerId) { + // CHECK(Number.isSafeInteger(triggerId)) + // CHECK(triggerId > 0) + async_uid_fields[kInitTriggerId] = triggerId; +} + + +function emitInitS(asyncId, type, triggerId, resource) { + // 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). + // Even though it bypasses all the argument checks. The performance savings + // here is critical. + 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 (!Number.isSafeInteger(triggerId)) { + if (triggerId !== undefined) + resource = triggerId; + triggerId = initTriggerId(); + } + + // I'd prefer allowing these checks to not exist, or only throw in a debug + // build, in order to improve performance. + if (!Number.isSafeInteger(asyncId) || asyncId < 0) + throw new RangeError('asyncId must be an unsigned integer'); + if (typeof type !== 'string' || type.length <= 0) + throw new TypeError('type must be a string with length > 0'); + if (!Number.isSafeInteger(triggerId) || triggerId < 0) + throw new RangeError('triggerId must be an unsigned integer'); + + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][init_symbol] === 'function') { + runInitCallback( + active_hooks_array[i][init_symbol], asyncId, type, triggerId, resource); + } + } + processing_hook = false; + + // Isn't null if hooks were added/removed while the hooks were running. + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +function emitBeforeN(asyncId) { + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][before_symbol] === 'function') { + runCallback(active_hooks_array[i][before_symbol], asyncId); + } + } + processing_hook = false; + + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +// Usage: emitBeforeS(asyncId[, triggerId]). If triggerId is omitted then +// asyncId will be used instead. +function emitBeforeS(asyncId, triggerId = asyncId) { + // CHECK(Number.isSafeInteger(asyncId) && asyncId > 0) + // CHECK(Number.isSafeInteger(triggerId) && triggerId > 0) + + // Validate the ids. + if (asyncId < 0 || triggerId < 0) { + fatalError('before(): asyncId or triggerId is less than zero ' + + `(asyncId: ${asyncId}, triggerId: ${triggerId})`); + } + + pushAsyncIds(asyncId, triggerId); + + if (async_hook_fields[kBefore] === 0) { + return; + } + + emitBeforeN(asyncId); +} + + +// Called from native. The asyncId stack handling is taken care of there before +// this is called. +function emitAfterN(asyncId) { + if (async_hook_fields[kAfter] > 0) { + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][after_symbol] === 'function') { + runCallback(active_hooks_array[i][after_symbol], asyncId); + } + } + processing_hook = false; + } + + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +// TODO(trevnorris): Calling emitBefore/emitAfter from native can't adjust the +// kIdStackIndex. But what happens if the user doesn't have both before and +// after callbacks. +function emitAfterS(asyncId) { + emitAfterN(asyncId); + popAsyncIds(asyncId); +} + + +function emitDestroyS(asyncId) { + // Return early if there are no destroy callbacks, or on attempt to emit + // destroy on the void. + if (async_hook_fields[kDestroy] === 0 || asyncId === 0) + return; + async_wrap.addIdToDestroyList(asyncId); +} + + +function emitDestroyN(asyncId) { + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][destroy_symbol] === 'function') { + runCallback(active_hooks_array[i][destroy_symbol], asyncId); + } + } + processing_hook = false; + + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +// Emit callbacks for native calls. Since some state can be setup directly from +// C++ there's no need to perform all the work here. + +// This should only be called if hooks_array has kInit > 0. There are no global +// values to setup. Though hooks_array will be cloned if C++ needed to call +// init(). +// TODO(trevnorris): Perhaps have MakeCallback call a single JS function that +// does the before/callback/after calls to remove two additional calls to JS. +function init(asyncId, type, resource, triggerId) { + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][init_symbol] === 'function') { + runInitCallback( + active_hooks_array[i][init_symbol], asyncId, type, triggerId, resource); + } + } + processing_hook = false; +} + + +// Generalized callers for all callbacks that handles error handling. + +// If either runInitCallback() or runCallback() throw then force the +// application to shutdown if one of the callbacks throws. This may change in +// the future depending on whether it can be determined if there's a slim +// chance of the application remaining stable after handling one of these +// exceptions. + +function runInitCallback(cb, asyncId, type, triggerId, resource) { + try { + cb(asyncId, type, triggerId, resource); + } catch (e) { + fatalError(e); + } +} + + +function runCallback(cb, asyncId) { + try { + cb(asyncId); + } catch (e) { + fatalError(e); + } +} + + +// Placing all exports down here because the exported classes won't export +// otherwise. +module.exports = { + // Public API + createHook, + currentId, + triggerId, + // Embedder API + AsyncEvent, + runInAsyncIdScope, + // Sensitive Embedder API + newUid, + initTriggerId, + setInitTriggerId, + emitInit: emitInitS, + emitBefore: emitBeforeS, + emitAfter: emitAfterS, + emitDestroy: emitDestroyS, +}; |