summaryrefslogtreecommitdiff
path: root/lib/async_hooks.js
diff options
context:
space:
mode:
authorTrevor Norris <trev.norris@gmail.com>2017-03-09 16:13:34 -0700
committerAnna Henningsen <anna@addaleax.net>2017-05-10 22:22:26 +0200
commit7e3a3c962f09233c53cee7ebe381341d7c8b7162 (patch)
tree02963f7724ed9099a3566f13b16619fddffe0793 /lib/async_hooks.js
parentc0bde73f1bbfedd4e77ddf87cf0bcec7bac9a61e (diff)
downloadnode-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.js488
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,
+};