diff --git a/lib/_http_agent.js b/lib/_http_agent.js index eebdb242463b5d..91d9b6272ec137 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -4,6 +4,7 @@ const net = require('net'); const util = require('util'); const EventEmitter = require('events'); const debug = util.debuglog('http'); +const setInitTriggerId = require('async_hooks').setInitTriggerId; // New Agent code. @@ -72,6 +73,7 @@ function Agent(options) { self.freeSockets[name] = freeSockets; socket.setKeepAlive(true, self.keepAliveMsecs); socket.unref(); + socket._asyncId = -1; socket._httpMessage = null; self.removeSocket(socket, options); freeSockets.push(socket); @@ -142,6 +144,8 @@ Agent.prototype.addRequest = function addRequest(req, options) { if (freeLen) { // we have a free socket, so use that. var socket = this.freeSockets[name].shift(); + // Assign the handle a new asyncId and run any init() hooks. + socket._handle.asyncReset(); debug('have free socket'); // don't leak @@ -154,8 +158,11 @@ Agent.prototype.addRequest = function addRequest(req, options) { } else if (sockLen < this.maxSockets) { debug('call onSocket', sockLen, freeLen); // If we are under maxSockets create a new one. + // Agent#addRequest() is only called from the ClientRequest constructor. So + // it's unnecessary to set the parent id before calling createSocket(). this.createSocket(req, options, function(err, newSocket) { if (err) { + setInitTriggerId(newSocket._handle.getAsyncId()); process.nextTick(function() { req.emit('error', err); }); @@ -269,6 +276,7 @@ Agent.prototype.removeSocket = function removeSocket(s, options) { // If we have pending requests and a socket gets closed make a new one this.createSocket(req, options, function(err, newSocket) { if (err) { + setInitTriggerId(newSocket._handle.getAsyncId()); process.nextTick(function() { req.emit('error', err); }); diff --git a/lib/_http_client.js b/lib/_http_client.js index cc2435089a9f3e..bcd096c0558792 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -13,6 +13,7 @@ const debug = common.debug; const OutgoingMessage = require('_http_outgoing').OutgoingMessage; const Agent = require('_http_agent'); const Buffer = require('buffer').Buffer; +const setInitTriggerId = require('async_hooks').setInitTriggerId; const urlToOptions = require('internal/url').urlToOptions; // The actual list of disallowed characters in regexp form is more like: @@ -563,6 +564,9 @@ function responseKeepAlive(res, req) { socket.removeListener('close', socketCloseListener); socket.removeListener('error', socketErrorListener); socket.once('error', freeSocketErrorListener); + // There are cases where _handle === null. Avoid those. + if (socket._handle) + setInitTriggerId(socket._handle.getAsyncId()); // Mark this socket as available, AFTER user-added end // handlers have a chance to run. process.nextTick(emitFreeNT, socket); diff --git a/lib/_http_common.js b/lib/_http_common.js index b8724f00ec6557..adb1658f44ffdc 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -18,6 +18,8 @@ exports.chunkExpression = /(?:^|\W)chunked(?:$|\W)/i; exports.continueExpression = /(?:^|\W)100-continue(?:$|\W)/i; exports.methods = methods; +const emitDestroy = require('async_hooks').emitDestroy; + const kOnHeaders = HTTPParser.kOnHeaders | 0; const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; const kOnBody = HTTPParser.kOnBody | 0; @@ -196,8 +198,14 @@ function freeParser(parser, req, socket) { parser.incoming = null; parser.outgoing = null; parser[kOnExecute] = null; - if (parsers.free(parser) === false) + if (parsers.free(parser) === false) { parser.close(); + } else { + // Since the Parser destructor isn't going to run the destroy() callbacks + // need to be triggered manually. + emitDestroy(parser.getAsyncId()); + } + parser = null; } if (req) { req.parser = null; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 30011e091525a3..25738a6220229f 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -9,6 +9,7 @@ const Buffer = require('buffer').Buffer; const common = require('_http_common'); const checkIsHttpToken = common._checkIsHttpToken; const checkInvalidHeaderChar = common._checkInvalidHeaderChar; +const setInitTriggerId = require('async_hooks').setInitTriggerId; const CRLF = common.CRLF; const debug = common.debug; @@ -153,8 +154,10 @@ function _writeRaw(data, encoding, callback) { if (this.output.length) { this._flushOutput(conn); } else if (!data.length) { - if (typeof callback === 'function') + if (typeof callback === 'function') { + setInitTriggerId(this.socket._asyncId); process.nextTick(callback); + } return true; } // Directly write to socket. @@ -477,6 +480,7 @@ const crlf_buf = Buffer.from('\r\n'); OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { if (this.finished) { var err = new Error('write after end'); + setInitTriggerId(this.socket._asyncId); process.nextTick(writeAfterEndNT, this, err, callback); return true; diff --git a/lib/async_hooks.js b/lib/async_hooks.js new file mode 100644 index 00000000000000..6461c146807f16 --- /dev/null +++ b/lib/async_hooks.js @@ -0,0 +1,522 @@ +'use strict'; + +const async_wrap = process.binding('async_wrap'); +// A Uint32Array wrapping node::Environment::AsyncHooks::fields_. An array that +// communicates between JS and C++ how many of a given type of callback are +// available to be called. +const async_hook_fields = async_wrap.async_hook_fields; +// A Float64Array wrapping node::Environment::AsyncHooks::uid_fields_. An array +// that communicates the state of the currentId and triggerId. Fields are as +// follows: +// kAsyncUidCntr: Maintain state of next unique id. +// kInitTriggerId: Written to just before creating a new resource, so the +// constructor knows what other resource is responsible for its init(). +// kScopedTriggerId: Hold the init triggerId for all constructors that run +// within triggerIdScope(). +// kIdStackIndex: TODO(trevnorris): add explanation +// kIdStackSize: TODO(trevnorris): add explanation +const async_uid_fields = async_wrap.async_uid_fields; +// Functions needed to swap the stack if it grows to large. +const { genIdArray, trimIdArray } = async_wrap; +// Stack of scoped trigger id's for triggerIdScope() +const trigger_scope_stack = []; +// 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 if processing hook callbacks. Used to make sure active_hooks_array +// isn't altered in mid execution if another hook is added or removed. +var processing_hooks = 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; + +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'); + +// 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. +// The exception is kActiveHooks. Which tracks the total number of AsyncEvents +// that exist on "active_hooks_array". +const { kInit, kBefore, kAfter, kDestroy, kActiveHooks, kIdStackIndex, + kIdStackSize, kIdStackLimit, kAsyncUidCntr, kInitTriggerId, + kScopedTriggerId } = async_wrap.constants; + +// 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: emitDestroyFromNative }); + +// Used to fatally abort the process if a callback throws. +function fatalError(e) { + if (e instanceof Error) { + process._rawDebug(e.stack); + } else { + const o = { message: e }; + Error.captureStackTrace(o, fatalError); + process._rawDebug(o.stack); + } + if (process.execArgv.indexOf('--abort-on-uncaught-exception') >= 0) + process.abort(); + process.exit(1); +} + + +// Public API // + +class AsyncHook { + constructor(fns) { + this[init_symbol] = fns.init; + this[before_symbol] = fns.before; + this[after_symbol] = fns.after; + this[destroy_symbol] = fns.destroy; + } + + enable() { + // Use local reference in case the pointer to the array needs to change. + var hooks_array = active_hooks_array; + var hook_fields = async_hook_fields; + + if (processing_hooks) { + if (tmp_active_hooks_array === null) + setupTmpActiveHooks(); + hooks_array = tmp_active_hooks_array; + hook_fields = tmp_async_hook_fields; + } + + if (hooks_array.indexOf(this) !== -1) + 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]; + hook_fields[kActiveHooks]++; + hooks_array.push(this); + return this; + } + + disable() { + // Use local reference in case the pointer to the array needs to change. + var hooks_array = active_hooks_array; + var hook_fields = async_hook_fields; + + if (processing_hooks) { + if (tmp_active_hooks_array === null) + setupTmpActiveHooks(); + hooks_array = tmp_active_hooks_array; + hook_fields = tmp_async_hook_fields; + } + + 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]; + hook_fields[kActiveHooks]--; + hooks_array.splice(index, 1); + return this; + } +} + + +function setupTmpActiveHooks() { + tmp_active_hooks_array = active_hooks_array.slice(); + // Don't want to make the assumption that kInit to kActiveHooks are + // indexes 0 to 5. 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]; + tmp_async_hook_fields[kActiveHooks] = async_hook_fields[kActiveHooks]; +} + + +// 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]; + async_hook_fields[kActiveHooks] = tmp_async_hook_fields[kActiveHooks]; + + tmp_active_hooks_array = null; + tmp_async_hook_fields = null; +} + + +function createHook(fns) { + if (fns.init !== undefined && typeof fns.init !== 'function') + throw new TypeError('init must be a function'); + if (fns.before !== undefined && typeof fns.before !== 'function') + throw new TypeError('before must be a function'); + if (fns.after !== undefined && typeof fns.after !== 'function') + throw new TypeError('after must be a function'); + if (fns.destroy !== undefined && typeof fns.destroy !== 'function') + throw new TypeError('destroy must be a function'); + + return new AsyncHook(fns); +} + + +function currentId() { + return async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]]; +} + + +function triggerId() { + return async_wrap.async_id_stack[async_hook_fields[kIdStackIndex] + 1]; +} + + +// Embedder API // + +class AsyncEvent { + constructor(type, triggerId) { + this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + // Do this before early return to null any potential kInitTriggerId. + 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. Checking for kInit covers + // checking for kActiveHooks. Doing this before the checks for performance. + 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'); + + 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); + } + } + } + + 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 triggerIdScope(id, cb) { + if (async_uid_fields[kScopedTriggerId] > 0) + trigger_scope_stack.push(async_uid_fields[kScopedTriggerId]); + try { + cb(); + } finally { + if (trigger_scope_stack.length > 0) + trigger_scope_stack.pop(); + } +} + + +// 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]; + if (tId <= 0) tId = async_uid_fields[kScopedTriggerId]; + // Reset value after it's been called so the next constructor doesn't + // inherit it by accident. + else async_uid_fields[kInitTriggerId] = 0; + if (tId <= 0) + tId = async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]]; + return tId; +} + + +function setInitTriggerId(id) { + async_uid_fields[kInitTriggerId] = id; +} + + +// TODO(trevnorris): Change the API to be: +// emitInit(number, string[, number][, resource]) +// Usage: +// emitInit(number, string[, number[, handle]]) +// +// 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. +// +// Note: All the emit* below end with "S" so they can be called by the methods +// on AsyncEvent, but are not exported that way. +function emitInitS(id, type, triggerId, handle) { + // Return immediately if there's nothing to do. Checking for kInit covers + // checking for kActiveHooks. Doing this before the checks for performance. + 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 (triggerId === undefined) + triggerId = initTriggerId(); + + // I'd prefer allowing these checks to not exist, or only throw in a debug + // build, to improve performance. + if (!Number.isSafeInteger(id) || id < 0) + throw new RangeError('id 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'); + + 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], id, type, triggerId, handle); + } + } + + // Isn't null if hooks were added/removed while the hooks were running. + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +function emitBeforeN(id, triggerId) { + 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], id); + } + } + + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +// Usage: emitBeforeS(id[, triggerId]). If triggerId is omitted then id will be +// used instead. +function emitBeforeS(id, triggerId) { + // CHECK(Number.isSafeInteger(id) && id > 0) + // CHECK(Number.isSafeInteger(triggerId) && triggerId > 0) + + // Validate the ids. + if (id < 0 || triggerId < 0) { + fatalError('before(): id or triggerId is less than zero ' + + `(id: ${id}, triggerId: ${triggerId})`); + } + + async_hook_fields[kIdStackIndex] += 2; + async_hook_fields[kIdStackSize] += 2; + + // Doing the assignment first because hitting another stack is irregular. + if (async_hook_fields[kIdStackIndex] >= kIdStackLimit) { + // This call: + // - Creates a new Float64Array and assigns it to async_wrap.async_id_stack + // - Saves the double* to Environment::AsyncHooks + // - Sets kIdStackIndex = 0. + genIdArray(); + } + + // CHECK(async_hook_fields[kIdStackSize] % kIdStackLimit === + // async_hook_fields[kIdStackIndex]) + + // Even indexes are id, odd indexes are triggerId + async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]] = id; + async_wrap.async_id_stack[async_hook_fields[kIdStackIndex] + 1] = + triggerId === undefined ? id : triggerId; + + if (async_hook_fields[kBefore] === 0) { + return; + } + + emitBeforeN(id, triggerId); +} + + +function emitAfterN(id) { + if (async_hook_fields[kAfter] > 0) { + 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], id); + } + } + } + + 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(id) { + // CHECK(Number.isSafeInteger(id) && id > 0) + // CHECK(id === async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]]) + if (id !== async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]]) { + fatalError('async hook stack has become corrupted (actual: ' + + async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]] + + `, expected: ${id}`); + } + + emitAfterN(id); + + // CHECK(async_hook_fields[kIdStackSize] >= 2) + + async_hook_fields[kIdStackIndex] -= 2; + async_hook_fields[kIdStackSize] -= 2; + + if (async_hook_fields[kIdStackIndex] === 0 && + async_hook_fields[kIdStackSize] > 0) { + // This call: + // - Replaces async_wrap.async_id_stack with the previous one in the stack. + // - Neuters the ArrayBuffer for the previous Float64Array + // - Resets the double* in Environment::AsyncHooks + trimIdArray(); + } +} + + +function emitDestroyS(id) { + // Return early if there are no destroy callbacks, or on attempt to emit + // destroy on the void. + if (async_hook_fields[kDestroy] === 0 || id === 0) + return; + async_wrap.addIdToDestroyList(id); +} + + +function emitDestroyFromNative(id) { + 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], id); + } + } + + 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 visit +// here since it's faster to do in JS. +// 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(id, type, handle) { + // Use initTriggerId() here so that native versions of initTriggerId() and + // triggerIdScope(), or so any triggerId set in JS propagates properly. + const triggerId = initTriggerId(); + 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], id, type, triggerId, handle); + } + } +} + + +// Generalized caller 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, id, type, triggerId, handle) { + processing_hooks = true; + try { + cb(id, type, triggerId, handle); + } catch (e) { + fatalError(e); + } +} + + +function runCallback(cb, id) { + processing_hooks = true; + try { + cb(id); + } 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, + triggerIdScope, + // Sensitive Embedder API + newUid, + initTriggerId, + setInitTriggerId, + emitInit: emitInitS, + emitBefore: emitBeforeS, + emitAfter: emitAfterS, + emitDestroy: emitDestroyS, +}; diff --git a/lib/dgram.js b/lib/dgram.js index 39d43092e15832..1958ad728a545b 100644 --- a/lib/dgram.js +++ b/lib/dgram.js @@ -4,6 +4,7 @@ const assert = require('assert'); const Buffer = require('buffer').Buffer; const util = require('util'); const EventEmitter = require('events'); +const setInitTriggerId = require('async_hooks').setInitTriggerId; const UV_UDP_REUSEADDR = process.binding('constants').os.UV_UDP_REUSEADDR; const UDP = process.binding('udp_wrap').UDP; @@ -89,6 +90,7 @@ function Socket(type, listener) { this._handle = handle; this._receiving = false; this._bindState = BIND_STATE_UNBOUND; + this._asyncId = handle.getAsyncId(); this.type = type; this.fd = null; // compatibility hack @@ -378,6 +380,7 @@ function doSend(ex, self, ip, list, address, port, callback) { return; } + setInitTriggerId(self._asyncId); var req = new SendWrap(); req.list = list; // Keep reference alive. req.address = address; diff --git a/lib/fs.js b/lib/fs.js index 573ffec05992aa..ae8f71592b4db4 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -20,6 +20,8 @@ const assertEncoding = internalFS.assertEncoding; const stringToFlags = internalFS.stringToFlags; const SyncWriteStream = internalFS.SyncWriteStream; +const { currentId, setInitTriggerId } = require('async_hooks'); + Object.defineProperty(exports, 'constants', { configurable: false, enumerable: true, @@ -37,6 +39,9 @@ const isWindows = process.platform === 'win32'; const DEBUG = process.env.NODE_DEBUG && /fs/.test(process.env.NODE_DEBUG); const errnoException = util._errnoException; +// Used to pull the id and triggerId from getIdsFromFd(). +const async_ids = binding.fd_async_ids; + function getOptions(options, defaultOptions) { if (options === null || options === undefined || typeof options === 'function') { @@ -129,6 +134,16 @@ function isFd(path) { return (path >>> 0) === path; } +// Helper function to reduce clutter. +function setActualInitTriggerId(self) { + if (self._asyncId > 0) { + setInitTriggerId(self._asyncId); + return; + } + binding.getIdsFromFd(self.fd); + setInitTriggerId(async_ids[0] > 0 ? async_ids[0] : currentId()); +} + // Static method to set the stats properties on a Stats object. fs.Stats = function( dev, @@ -265,16 +280,23 @@ fs.readFile = function(path, options, callback) { req.oncomplete = readFileAfterOpen; if (context.isUserFd) { + // If the fd was opened synchronously then there will be no asyncId. + binding.getIdsFromFd(path); + context._asyncId = async_ids[0] > 0 ? async_ids[0] : currentId(); + setInitTriggerId(context._asyncId); process.nextTick(function() { req.oncomplete(null, path); }); return; } + // No need to setInitTriggerId() here since the triggerId and currentId are + // the same. binding.open(pathModule._makeLong(path), stringToFlags(options.flag || 'r'), 0o666, req); + context._asyncId = req.getAsyncId(); }; const kReadFileBufferLength = 8 * 1024; @@ -289,6 +311,10 @@ function ReadFileContext(callback, encoding) { this.pos = 0; this.encoding = encoding; this.err = null; + // No need to store _triggerId because it won't be passed on to other + // resources, and the value is stored in the ReqWrap for the call to + // MakeCallback. + this._asyncId = -1; } ReadFileContext.prototype.read = function() { @@ -310,7 +336,12 @@ ReadFileContext.prototype.read = function() { req.oncomplete = readFileAfterRead; req.context = this; + setActualInitTriggerId(this); binding.read(this.fd, buffer, offset, length, -1, req); + // Since a ReadFileContext encapsulates many interactions with a file, update + // the _asyncId of the instance to the latest interaction so that it properly + // propagates to the next action's triggerId. + this._asyncId = req.getAsyncId(); }; ReadFileContext.prototype.close = function(err) { @@ -319,6 +350,9 @@ ReadFileContext.prototype.close = function(err) { req.context = this; this.err = err; + // This will be needed for either the nextTick() or .close(). + setActualInitTriggerId(this); + if (this.isUserFd) { process.nextTick(function() { req.oncomplete(null); @@ -327,6 +361,7 @@ ReadFileContext.prototype.close = function(err) { } binding.close(this.fd, req); + this._asyncId = req.getAsyncId(); }; function readFileAfterOpen(err, fd) { @@ -1656,6 +1691,10 @@ function ReadStream(path, options) { this.autoClose = options.autoClose === undefined ? true : options.autoClose; this.pos = undefined; this.bytesRead = 0; + // If no fd or asyncId is passed then assume that .open() will be called and + // that _asyncId will be assigned at that time. + this._asyncId = -1; + if (this.start !== undefined) { if (typeof this.start !== 'number') { @@ -1735,6 +1774,7 @@ ReadStream.prototype._read = function(n) { // the actual read. var self = this; + setActualInitTriggerId(this); fs.read(this.fd, pool, pool.used, toRead, this.pos, onread); // move the pool positions, and internal position for reading. @@ -1778,12 +1818,14 @@ ReadStream.prototype.close = function(cb) { this.once('open', close); return; } + setActualInitTriggerId(this); return process.nextTick(() => this.emit('close')); } this.closed = true; close(); function close(fd) { + setActualInitTriggerId(self); fs.close(fd || self.fd, function(er) { if (er) self.emit('error', er); @@ -1873,6 +1915,7 @@ WriteStream.prototype._write = function(data, encoding, cb) { }); var self = this; + setActualInitTriggerId(this); fs.write(this.fd, data, 0, data.length, this.pos, function(er, bytes) { if (er) { if (self.autoClose) { @@ -1919,6 +1962,7 @@ WriteStream.prototype._writev = function(data, cb) { size += chunk.length; } + setActualInitTriggerId(this); writev(this.fd, chunks, this.pos, function(er, bytes) { if (er) { self.destroy(); diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index 71a6c529041b47..1631fdf9d56da3 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -281,6 +281,11 @@ } function setupProcessFatal() { + const async_wrap = process.binding('async_wrap'); + const async_hook_fields = async_wrap.async_hook_fields; + const resetIdArray = async_wrap.resetIdArray; + const { kAfter, kIdStackIndex, kIdStackSize } = + async_wrap.constants; process._fatalException = function(er) { var caught; @@ -303,9 +308,20 @@ // nothing to be done about it at this point. } - // if we handled an error, then make sure any ticks get processed } else { + // If we handled an error, then make sure any ticks get processed NativeModule.require('timers').setImmediate(process._tickCallback); + + // Emit the after() hooks now that the exception has been delt with. + if (async_hook_fields[kAfter] > 0) { + while (async_hook_fields[kIdStackSize] > 0) { + NativeModule.require('async_hooks').emitAfter( + async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]]); + } + // Or completely reset the id stack. + } else { + resetIdArray(); + } } return caught; diff --git a/lib/internal/cluster/child.js b/lib/internal/cluster/child.js index 3275eec720c738..919cb0728b8ef7 100644 --- a/lib/internal/cluster/child.js +++ b/lib/internal/cluster/child.js @@ -145,7 +145,13 @@ function rr(message, indexesKey, cb) { // with it. Fools net.Server into thinking that it's backed by a real // handle. Use a noop function for ref() and unref() because the control // channel is going to keep the worker alive anyway. - const handle = { close, listen, ref: noop, unref: noop }; + // + // Emulate AsyncWrap::GetAsyncId() by returning a value from getAsyncId(). + // TODO(trevnorris): Right now just return -2 so locations where this is + // propagating can be identified, if there are any. Since this is a faux + // handle not sure how to treat it (i.e. should it receive its own uid?). + const handle = { + close, listen, ref: noop, unref: noop, getAsyncId: () => -2 }; if (message.sockname) { handle.getsockname = getsockname; // TCP handles only. diff --git a/lib/internal/cluster/shared_handle.js b/lib/internal/cluster/shared_handle.js index c0663772426213..4d95b75659f0fc 100644 --- a/lib/internal/cluster/shared_handle.js +++ b/lib/internal/cluster/shared_handle.js @@ -19,10 +19,12 @@ function SharedHandle(key, address, port, addressType, fd, flags) { else rval = net._createServerHandle(address, port, addressType, fd); - if (typeof rval === 'number') + if (typeof rval === 'number') { this.errno = rval; - else + } else { this.handle = rval; + this.getAsyncId = this.handle.getAsyncId; + } } SharedHandle.prototype.add = function(worker, send) { diff --git a/lib/internal/module.js b/lib/internal/module.js index 2f38618daac5f7..f2c32b51e1106a 100644 --- a/lib/internal/module.js +++ b/lib/internal/module.js @@ -52,10 +52,10 @@ function stripBOM(content) { } exports.builtinLibs = [ - 'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns', - 'domain', 'events', 'fs', 'http', 'https', 'net', 'os', 'path', 'punycode', - 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'tls', 'tty', - 'url', 'util', 'v8', 'vm', 'zlib' + 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'crypto', + 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net', 'os', + 'path', 'punycode', 'querystring', 'readline', 'repl', 'stream', + 'string_decoder', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib' ]; function addBuiltinLibsToObject(object) { diff --git a/lib/internal/process/next_tick.js b/lib/internal/process/next_tick.js index f27ef622a96e6a..1b9c9190cc4b67 100644 --- a/lib/internal/process/next_tick.js +++ b/lib/internal/process/next_tick.js @@ -3,8 +3,20 @@ exports.setup = setupNextTick; function setupNextTick() { + const async_wrap = process.binding('async_wrap'); + const async_hooks = require('async_hooks'); const promises = require('internal/process/promises'); const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks); + const initTriggerId = async_hooks.initTriggerId; + // Two arrays that share state between C++ and JS. + const { async_uid_fields, async_hook_fields } = async_wrap; + // Two functions to manipulate the id stack. + const { genIdArray, trimIdArray } = async_wrap; + // The needed emit*() functions. + const { emitInit, emitBefore, emitAfter, emitDestroy } = async_hooks; + // Grab the constants necessary for working with internal arrays. + const { kInit, kBefore, kAfter, kDestroy, kAsyncUidCntr, kIdStackIndex, + kIdStackSize, kIdStackLimit } = async_wrap.constants; var nextTickQueue = []; var microtasksScheduled = false; @@ -44,10 +56,13 @@ function setupNextTick() { if (microtasksScheduled) return; - nextTickQueue.push({ - callback: runMicrotasksCallback, - domain: null - }); + const tickObject = + new TickObject(runMicrotasksCallback, undefined, null, true); + // For the moment all microtasks come from the void. Until the PromiseHook + // API is implemented in V8. + tickObject._asyncId = 0; + tickObject._triggerId = 0; + nextTickQueue.push(tickObject); tickInfo[kLength]++; microtasksScheduled = true; @@ -82,20 +97,67 @@ function setupNextTick() { } } + function nextTickEmitBefore(id, triggerId) { + if (async_hook_fields[kBefore] > 0) + return emitBefore(id, triggerId); + + // Do same basic operations to the id stack as emitBefore/emitAfter. + async_hook_fields[kIdStackIndex] += 2; + async_hook_fields[kIdStackSize] += 2; + + if (async_hook_fields[kIdStackIndex] >= kIdStackLimit) { + genIdArray(); + } + + async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]] = id; + async_wrap.async_id_stack[async_hook_fields[kIdStackIndex] + 1] = + triggerId === undefined ? id : triggerId; + } + + function nextTickEmitAfter(id) { + // CHECK(id > 1) + if (async_hook_fields[kAfter] > 0) + return emitAfter(id); + + async_hook_fields[kIdStackIndex] -= 2; + async_hook_fields[kIdStackSize] -= 2; + + if (async_hook_fields[kIdStackIndex] === 0 && + async_hook_fields[kIdStackSize] > 0) { + trimIdArray(); + } + } + // Run callbacks that have no domain. // Using domains will cause this to be overridden. function _tickCallback() { - var callback, args, tock; - do { while (tickInfo[kIndex] < tickInfo[kLength]) { - tock = nextTickQueue[tickInfo[kIndex]++]; - callback = tock.callback; - args = tock.args; + const tock = nextTickQueue[tickInfo[kIndex]++]; + const callback = tock.callback; + const args = tock.args; + + // CHECK(Number.isSafeInteger(tock._asyncId) && tock._asyncId > 0) + // CHECK(Number.isSafeInteger(tock._triggerId) && tock._triggerId > 0) + + nextTickEmitBefore(tock._asyncId, tock._triggerId); + // TODO(trevnorris): Checking kDestroy this way is a cheat because if + // any destroy() hooks are enabled during the callback then they won't + // be notified of this id's destroy. Though that may be alright since + // no other callbacks would have been called for that id. + // Emit destroy() here because if there's an error the destroy() still + // needs to be called for this id. Works because all destroy() are + // called asyncronously. + if (async_hook_fields[kDestroy] > 0) + emitDestroy(tock._asyncId); + // Using separate callback execution functions allows direct // callback invocation with small numbers of arguments to avoid the // performance hit associated with using `fn.apply()` _combinedTickCallback(args, callback); + + nextTickEmitAfter(tock._asyncId); + if (1e4 < tickInfo[kIndex]) tickDone(); } @@ -106,20 +168,32 @@ function setupNextTick() { } function _tickDomainCallback() { - var callback, domain, args, tock; - do { while (tickInfo[kIndex] < tickInfo[kLength]) { - tock = nextTickQueue[tickInfo[kIndex]++]; - callback = tock.callback; - domain = tock.domain; - args = tock.args; + const tock = nextTickQueue[tickInfo[kIndex]++]; + const callback = tock.callback; + const domain = tock.domain; + const args = tock.args; if (domain) domain.enter(); + + // CHECK(Number.isSafeInteger(tock._asyncId) && tock._asyncId > 0) + // CHECK(Number.isSafeInteger(tock._triggerId) && tock._triggerId > 0) + + nextTickEmitBefore(tock._asyncId, tock._triggerId); + // Emit destroy() here because if there's an error the destroy() still + // needs to be called for this id. Works because all destroy() are + // called asyncronously. + if (async_hook_fields[kDestroy] > 0) + emitDestroy(tock._asyncId); + // Using separate callback execution functions allows direct // callback invocation with small numbers of arguments to avoid the // performance hit associated with using `fn.apply()` _combinedTickCallback(args, callback); + + nextTickEmitAfter(tock._asyncId); + if (1e4 < tickInfo[kIndex]) tickDone(); if (domain) @@ -131,6 +205,26 @@ function setupNextTick() { } while (tickInfo[kLength] !== 0); } + function TickObject(callback, args, domain, isMicrotask) { + this.callback = callback; + this.domain = domain; + this.args = args; + this._asyncId = -1; + this._triggerId = -1; + if (!isMicrotask) + setupInit(this); + } + + // This function is for performance reasons. If the code here is placed + // directly in TickObject it would be over the maximum character count + // to be inlined, and it would suffer a penalty lose. + function setupInit(self) { + self._asyncId = ++async_uid_fields[kAsyncUidCntr]; + self._triggerId = initTriggerId(); + if (async_hook_fields[kInit] > 0) + emitInit(self._asyncId, 'TickObject', self._triggerId, self); + } + function nextTick(callback) { if (typeof callback !== 'function') throw new TypeError('callback is not a function'); @@ -145,11 +239,8 @@ function setupNextTick() { args[i - 1] = arguments[i]; } - nextTickQueue.push({ - callback, - domain: process.domain || null, - args - }); + nextTickQueue.push( + new TickObject(callback, args, process.domain || null, false)); tickInfo[kLength]++; } } diff --git a/lib/net.js b/lib/net.js index e4f97ab80debd5..ba1675f2ab33e3 100644 --- a/lib/net.js +++ b/lib/net.js @@ -19,6 +19,7 @@ const PipeConnectWrap = process.binding('pipe_wrap').PipeConnectWrap; const ShutdownWrap = process.binding('stream_wrap').ShutdownWrap; const WriteWrap = process.binding('stream_wrap').WriteWrap; +const setInitTriggerId = require('async_hooks').setInitTriggerId; var cluster; const errnoException = util._errnoException; @@ -114,6 +115,7 @@ function initSocketHandle(self) { if (self._handle) { self._handle.owner = self; self._handle.onread = onread; + self._asyncId = self._handle.getAsyncId(); // If handle doesn't support writev - neither do we if (!self._handle.writev) @@ -129,6 +131,7 @@ function Socket(options) { if (!(this instanceof Socket)) return new Socket(options); this.connecting = false; + this._asyncId = -1; this._hadError = false; this._handle = null; this._parent = null; @@ -143,9 +146,11 @@ function Socket(options) { if (options.handle) { this._handle = options.handle; // private + this._asyncId = this._handle.getAsyncId(); } else if (options.fd !== undefined) { this._handle = createHandle(options.fd); this._handle.open(options.fd); + this._asyncId = this._handle.getAsyncId(); if ((options.fd == 1 || options.fd == 2) && (this._handle instanceof Pipe) && process.platform === 'win32') { @@ -228,6 +233,7 @@ function onSocketFinish() { if (!this._handle || !this._handle.shutdown) return this.destroy(); + setInitTriggerId(this._asyncId); var req = new ShutdownWrap(); req.oncomplete = afterShutdown; req.handle = this._handle; @@ -296,6 +302,7 @@ function writeAfterFIN(chunk, encoding, cb) { // TODO: defer error events consistently everywhere, not just the cb this.emit('error', er); if (typeof cb === 'function') { + setInitTriggerId(this._asyncId); process.nextTick(cb, er); } } @@ -854,6 +861,7 @@ function connect(self, address, port, addressType, localAddress, localPort) { } if (addressType === 6 || addressType === 4) { + setInitTriggerId(self._asyncId); const req = new TCPConnectWrap(); req.oncomplete = afterConnect; req.address = address; @@ -867,6 +875,7 @@ function connect(self, address, port, addressType, localAddress, localPort) { err = self._handle.connect6(req, address, port); } else { + setInitTriggerId(self._asyncId); const req = new PipeConnectWrap(); req.address = address; req.oncomplete = afterConnect; @@ -968,6 +977,7 @@ function lookupAndConnect(self, options) { // If host is an IP, skip performing a lookup var addressType = exports.isIP(host); if (addressType) { + setInitTriggerId(self._asyncId); process.nextTick(function() { if (self.connecting) connect(self, host, port, addressType, localAddress, localPort); @@ -1061,6 +1071,7 @@ function afterConnect(status, handle, req, readable, writable) { // Update handle if it was wrapped // TODO(indutny): assert that the handle is actually an ancestor of old one + // TODO(trevnorris): Doesn't look like this does anything. Verify and remove. handle = self._handle; debug('afterConnect'); @@ -1137,6 +1148,7 @@ function Server(options, connectionListener) { configurable: true, enumerable: false }); + this._asyncId = -1; this._handle = null; this._usingSlaves = false; this._slaves = []; @@ -1252,6 +1264,7 @@ Server.prototype._listen2 = function(address, port, addressType, backlog, fd) { this._handle = rval; } + this._asyncId = this._handle.getAsyncId(); this._handle.onconnection = onconnection; this._handle.owner = this; @@ -1261,6 +1274,7 @@ Server.prototype._listen2 = function(address, port, addressType, backlog, fd) { var ex = exceptionWithHostPort(err, 'listen', address, port); this._handle.close(); this._handle = null; + setInitTriggerId(this._asyncId); process.nextTick(emitErrorNT, this, ex); return; } @@ -1272,6 +1286,7 @@ Server.prototype._listen2 = function(address, port, addressType, backlog, fd) { if (this._unref) this.unref(); + setInitTriggerId(this._asyncId); process.nextTick(emitListeningNT, this); }; @@ -1357,6 +1372,7 @@ Server.prototype.listen = function() { if (options instanceof TCP) { this._handle = options; + this._asyncId = this._handle.getAsyncId(); listen(this, null, -1, -1, backlog); } else if (typeof options.fd === 'number' && options.fd >= 0) { listen(this, null, null, null, backlog, options.fd); @@ -1455,7 +1471,10 @@ function onconnection(err, clientHandle) { Server.prototype.getConnections = function(cb) { + const self = this; + function end(err, connections) { + setInitTriggerId(self._asyncId); process.nextTick(cb, err, connections); } @@ -1534,6 +1553,7 @@ Server.prototype._emitCloseIfDrained = function() { return; } + setInitTriggerId(this._asyncId); process.nextTick(emitCloseNT, this); }; diff --git a/lib/timers.js b/lib/timers.js index 6d456da36faf67..34e60c06a23cfc 100644 --- a/lib/timers.js +++ b/lib/timers.js @@ -1,11 +1,25 @@ 'use strict'; +const async_wrap = process.binding('async_wrap'); const TimerWrap = process.binding('timer_wrap').Timer; const L = require('internal/linkedlist'); const assert = require('assert'); const util = require('util'); const debug = util.debuglog('timer'); const kOnTimeout = TimerWrap.kOnTimeout | 0; +// Two arrays that share state between C++ and JS. +const { async_uid_fields, async_hook_fields } = async_wrap; +// Two functions to manipulate the id stack. +const { genIdArray, trimIdArray } = async_wrap; +const { emitInit, emitBefore, emitAfter, emitDestroy, + initTriggerId } = require('async_hooks'); +// Grab the constants necessary for working with internal arrays. +const { kInit, kBefore, kAfter, kDestroy, kAsyncUidCntr, kIdStackIndex, + kIdStackSize, kIdStackLimit } = async_wrap.constants; + +const async_id_symbol = Symbol('asyncId'); +const trigger_id_symbol = Symbol('triggerId'); +const not_called_symbol = Symbol('notCalled'); // Timeout values > TIMEOUT_MAX are set to 1. const TIMEOUT_MAX = 2147483647; // 2^31-1 @@ -131,6 +145,17 @@ function insert(item, unrefed) { lists[msecs] = list = createTimersList(msecs, unrefed); } + // No UID, assign a new one. + if (!item[async_id_symbol]) { + item[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + item[trigger_id_symbol] = initTriggerId(); + // NOTE: While all "item" passed to insert() are emitted with type + // 'Timeout', the actual object can be created outside of the Timeout + // constructor. Even so, they'll all be emitted as 'Timeout'. + if (async_hook_fields[kInit] > 0) + emitInit(item[async_id_symbol], 'Timeout', item[trigger_id_symbol], item); + } + L.append(list, item); assert(!L.isEmpty(list)); // list is not empty } @@ -158,6 +183,36 @@ function TimersList(msecs, unrefed) { this.msecs = msecs; } +function timerEmitBefore(id, triggerId) { + if (async_hook_fields[kBefore] > 0) + return emitBefore(id, triggerId); + + // Do same basic operations to the id stack as emitBefore/emitAfter. + async_hook_fields[kIdStackIndex] += 2; + async_hook_fields[kIdStackSize] += 2; + + if (async_hook_fields[kIdStackIndex] >= kIdStackLimit) { + genIdArray(); + } + + async_wrap.async_id_stack[async_hook_fields[kIdStackIndex]] = id; + async_wrap.async_id_stack[async_hook_fields[kIdStackIndex] + 1] = + triggerId === undefined ? id : triggerId; +} + +function timerEmitAfter(id) { + if (async_hook_fields[kAfter] > 0) + return emitAfter(id); + + async_hook_fields[kIdStackIndex] -= 2; + async_hook_fields[kIdStackSize] -= 2; + + if (async_hook_fields[kIdStackIndex] === 0 && + async_hook_fields[kIdStackSize] > 0) { + trimIdArray(); + } +} + function listOnTimeout() { var list = this._list; var msecs = list.msecs; @@ -188,7 +243,9 @@ function listOnTimeout() { L.remove(timer); assert(timer !== L.peek(list)); - if (!timer._onTimeout) continue; + if (!timer._onTimeout) { + continue; + } var domain = timer.domain; if (domain) { @@ -361,6 +418,14 @@ function createSingleTimeout(callback, after, args) { function ontimeout(timer) { var args = timer._timerArgs; var callback = timer._onTimeout; + timerEmitBefore(timer[async_id_symbol], timer[trigger_id_symbol]); + // TODO(trevnorris): Like with nextTick, this is a bit of a hack to get + // around needing to store the id and wait to check for kDestroy until after + // the callback has run. This is to account for the callback possibly + // throwing an exception. + if (async_hook_fields[kDestroy] > 0 && timer[not_called_symbol]) + emitDestroy(timer[async_id_symbol]); + timer[not_called_symbol] = false; if (!args) callback.call(timer); else { @@ -380,6 +445,7 @@ function ontimeout(timer) { } if (timer._repeat) rearm(timer); + timerEmitAfter(timer[async_id_symbol]); } @@ -462,6 +528,11 @@ function Timeout(after, callback, args) { this._onTimeout = callback; this._timerArgs = args; this._repeat = null; + this[not_called_symbol] = true; + this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + this[trigger_id_symbol] = initTriggerId(); + if (async_hook_fields[kInit] > 0) + emitInit(this[async_id_symbol], 'Timeout', this[trigger_id_symbol], this); } @@ -522,6 +593,10 @@ Timeout.prototype.close = function() { } else { unenroll(this); } + if (async_hook_fields[kDestroy] && this[not_called_symbol]) { + emitDestroy(this[async_id_symbol]); + } + this[not_called_symbol] = false; return this; }; @@ -619,6 +694,10 @@ function processImmediate() { // 4.7) what is in this smaller function. function tryOnImmediate(immediate, oldTail) { var threw = true; + timerEmitBefore(immediate[async_id_symbol], immediate[trigger_id_symbol]); + if (async_hook_fields[kDestroy] > 0) + emitDestroy(immediate[async_id_symbol]); + immediate[not_called_symbol] = false; try { // make the actual call outside the try/catch to allow it to be optimized runCallback(immediate); @@ -640,6 +719,7 @@ function tryOnImmediate(immediate, oldTail) { process.nextTick(processImmediate); } } + timerEmitAfter(immediate[async_id_symbol]); } function runCallback(timer) { @@ -671,6 +751,11 @@ function Immediate() { this._argv = null; this._onImmediate = null; this.domain = process.domain; + this[not_called_symbol] = true; + this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + this[trigger_id_symbol] = initTriggerId(); + if (async_hook_fields[kInit]) + emitInit(this[async_id_symbol], 'Immediate', this[trigger_id_symbol], this); } exports.setImmediate = function(callback, arg1, arg2, arg3) { @@ -728,4 +813,9 @@ exports.clearImmediate = function(immediate) { if (!immediateQueue.head) { process._needImmediateCallback = false; } + + if (immediate[async_id_symbol] > 0 && immediate[not_called_symbol]) { + if (async_hook_fields[kDestroy]) + emitDestroy(immediate[async_id_symbol]); + } }; diff --git a/node.gyp b/node.gyp index 7d03d1df08a7a2..17022b14103804 100644 --- a/node.gyp +++ b/node.gyp @@ -24,6 +24,7 @@ 'lib/internal/bootstrap_node.js', 'lib/_debug_agent.js', 'lib/_debugger.js', + 'lib/async_hooks.js', 'lib/assert.js', 'lib/buffer.js', 'lib/child_process.js', diff --git a/src/async-wrap-inl.h b/src/async-wrap-inl.h index 64b5f091612368..f7da73edd22f62 100644 --- a/src/async-wrap-inl.h +++ b/src/async-wrap-inl.h @@ -15,18 +15,18 @@ namespace node { -inline bool AsyncWrap::ran_init_callback() const { - return static_cast(bits_ & 1); +inline AsyncWrap::ProviderType AsyncWrap::provider_type() const { + return provider_type_; } -inline AsyncWrap::ProviderType AsyncWrap::provider_type() const { - return static_cast(bits_ >> 1); +inline double AsyncWrap::get_id() const { + return id_; } -inline int64_t AsyncWrap::get_uid() const { - return uid_; +inline double AsyncWrap::get_trigger_id() const { + return trigger_id_; } diff --git a/src/async-wrap.cc b/src/async-wrap.cc index a0780566db72d8..b26d16df2477cb 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -9,13 +9,14 @@ #include "v8.h" #include "v8-profiler.h" -using v8::Boolean; +using v8::Array; +using v8::ArrayBuffer; using v8::Context; +using v8::Float64Array; using v8::Function; using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::HeapProfiler; -using v8::Int32; using v8::Integer; using v8::Isolate; using v8::Local; @@ -24,8 +25,11 @@ using v8::Number; using v8::Object; using v8::RetainedObjectInfo; using v8::TryCatch; +using v8::Uint32Array; using v8::Value; +using AsyncHooks = node::Environment::AsyncHooks; + namespace node { static const char* const provider_names[] = { @@ -36,6 +40,8 @@ static const char* const provider_names[] = { }; +// Report correct information in a heapdump. + class RetainedAsyncInfo: public RetainedObjectInfo { public: explicit RetainedAsyncInfo(uint16_t class_id, AsyncWrap* wrap); @@ -88,7 +94,9 @@ intptr_t RetainedAsyncInfo::GetSizeInBytes() { RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local wrapper) { // No class_id should be the provider type of NONE. - CHECK_NE(NODE_ASYNC_ID_OFFSET, class_id); + CHECK_GT(class_id, NODE_ASYNC_ID_OFFSET); + // And make sure the class_id doesn't extend past the last provider. + CHECK_LE(class_id - NODE_ASYNC_ID_OFFSET, AsyncWrap::PROVIDERS_LENGTH); CHECK(wrapper->IsObject()); CHECK(!wrapper.IsEmpty()); @@ -105,55 +113,41 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local wrapper) { // end RetainedAsyncInfo -static void EnableHooksJS(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Local init_fn = env->async_hooks_init_function(); - if (init_fn.IsEmpty() || !init_fn->IsFunction()) - return env->ThrowTypeError("init callback is not assigned to a function"); - env->async_hooks()->set_enable_callbacks(1); -} - - -static void DisableHooksJS(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - env->async_hooks()->set_enable_callbacks(0); -} - - static void SetupHooks(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - if (env->async_hooks()->callbacks_enabled()) - return env->ThrowError("hooks should not be set while also enabled"); if (!args[0]->IsObject()) return env->ThrowTypeError("first argument must be an object"); + // All of init, before, after, destroy are supplied by async_hooks + // internally, so this should every only be called once. At which time all + // the functions should be set. Detect this by checking if init !IsEmpty(). + CHECK(env->async_hooks_init_function().IsEmpty()); + Local fn_obj = args[0].As(); Local init_v = fn_obj->Get( env->context(), FIXED_ONE_BYTE_STRING(env->isolate(), "init")).ToLocalChecked(); - Local pre_v = fn_obj->Get( + Local before_v = fn_obj->Get( env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "pre")).ToLocalChecked(); - Local post_v = fn_obj->Get( + FIXED_ONE_BYTE_STRING(env->isolate(), "before")).ToLocalChecked(); + Local after_v = fn_obj->Get( env->context(), - FIXED_ONE_BYTE_STRING(env->isolate(), "post")).ToLocalChecked(); + FIXED_ONE_BYTE_STRING(env->isolate(), "after")).ToLocalChecked(); Local destroy_v = fn_obj->Get( env->context(), FIXED_ONE_BYTE_STRING(env->isolate(), "destroy")).ToLocalChecked(); - if (!init_v->IsFunction()) - return env->ThrowTypeError("init callback must be a function"); + CHECK(init_v->IsFunction()); + CHECK(before_v->IsFunction()); + CHECK(after_v->IsFunction()); + CHECK(destroy_v->IsFunction()); env->set_async_hooks_init_function(init_v.As()); - - if (pre_v->IsFunction()) - env->set_async_hooks_pre_function(pre_v.As()); - if (post_v->IsFunction()) - env->set_async_hooks_post_function(post_v.As()); - if (destroy_v->IsFunction()) - env->set_async_hooks_destroy_function(destroy_v.As()); + env->set_async_hooks_before_function(before_v.As()); + env->set_async_hooks_after_function(after_v.As()); + env->set_async_hooks_destroy_function(destroy_v.As()); } @@ -165,41 +159,132 @@ void AsyncWrap::Initialize(Local target, HandleScope scope(isolate); env->SetMethod(target, "setupHooks", SetupHooks); - env->SetMethod(target, "disable", DisableHooksJS); - env->SetMethod(target, "enable", EnableHooksJS); + env->SetMethod(target, "addIdToDestroyList", AddIdToDestroyList); + env->SetMethod(target, "genIdArray", GenIdArray); + env->SetMethod(target, "trimIdArray", TrimIdArray); + env->SetMethod(target, "resetIdArray", ResetIdArray); + + v8::PropertyAttribute ReadOnlyDontDelete = + static_cast(v8::ReadOnly | v8::DontDelete); + + // The stack of async and trigger ids will be kept in a Float64Array. With a + // size of 1024 (allowing for 512 recursive calls) it is very unlikely that + // a new array will ever need to be created, but just in case that occation + // comes we can handle it. This array will hold allocations that have filled. + target->ForceSet(context, + FIXED_ONE_BYTE_STRING(isolate, "async_id_stack_list"), + Array::New(isolate), + ReadOnlyDontDelete).FromJust(); + + // Attach the uint32_t[] where each slot contains the count of the number of + // callbacks waiting to be called on a particular event. It can then be + // incremented/decremented from JS quickly to communicate to C++ if there are + // any callbacks waiting to be called. + uint32_t* fields_ptr = env->async_hooks()->fields(); + int fields_count = env->async_hooks()->fields_count(); + Local fields_ab = + ArrayBuffer::New(isolate, fields_ptr, fields_count * sizeof(*fields_ptr)); + Local fields = + Uint32Array::New(fields_ab, 0, fields_count); + target->ForceSet(context, + FIXED_ONE_BYTE_STRING(isolate, "async_hook_fields"), + fields, + ReadOnlyDontDelete).FromJust(); + + // The following v8::Float64Array has 5 fields. These fields are shared in + // this way to allow JS and C++ to read/write each value as quickly as + // possible. The fields are represented as follows: + // + // kAsyncUid: Maintains the state of the next unique id to be assigned. + // + // kInitTriggerId: Write the id of the resource resource responsible for a + // handle's creation just before calling the new handle's constructor. + // After the new handle is constructed kInitTriggerId is set back to 0. + // + // kScopedTriggerId: triggerId for all constructors created within the + // execution scope of the JS function triggerIdScope(). This value is + // superseded by kInitTriggerId, if set. + double* uid_fields_ptr = env->async_hooks()->uid_fields(); + int uid_fields_count = env->async_hooks()->uid_fields_count(); + Local uid_fields_ab = ArrayBuffer::New( + isolate, + uid_fields_ptr, + uid_fields_count * sizeof(*uid_fields_ptr)); + Local uid_fields = + Float64Array::New(uid_fields_ab, 0, uid_fields_count); + target->ForceSet(context, + FIXED_ONE_BYTE_STRING(isolate, "async_uid_fields"), + uid_fields, + ReadOnlyDontDelete).FromJust(); + + Local constants = Object::New(isolate); +#define SET_HOOKS_CONSTANT(name) \ + constants->ForceSet(context, \ + FIXED_ONE_BYTE_STRING(isolate, #name), \ + Integer::New(isolate, AsyncHooks::name), \ + ReadOnlyDontDelete).FromJust(); + SET_HOOKS_CONSTANT(kInit); + SET_HOOKS_CONSTANT(kBefore); + SET_HOOKS_CONSTANT(kAfter); + SET_HOOKS_CONSTANT(kDestroy); + SET_HOOKS_CONSTANT(kActiveHooks); + SET_HOOKS_CONSTANT(kIdStackIndex); + SET_HOOKS_CONSTANT(kIdStackSize); + SET_HOOKS_CONSTANT(kAsyncUidCntr); + SET_HOOKS_CONSTANT(kInitTriggerId); + SET_HOOKS_CONSTANT(kScopedTriggerId); + SET_HOOKS_CONSTANT(kIdStackLimit); +#undef SET_HOOKS_CONSTANT + target->ForceSet(context, + FIXED_ONE_BYTE_STRING(isolate, "constants"), + constants, + ReadOnlyDontDelete).FromJust(); Local async_providers = Object::New(isolate); #define V(PROVIDER) \ - async_providers->Set(FIXED_ONE_BYTE_STRING(isolate, #PROVIDER), \ - Integer::New(isolate, AsyncWrap::PROVIDER_ ## PROVIDER)); + async_providers->ForceSet( \ + context, \ + FIXED_ONE_BYTE_STRING(isolate, #PROVIDER), \ + Integer::New(isolate, AsyncWrap::PROVIDER_ ## PROVIDER), \ + ReadOnlyDontDelete).FromJust(); NODE_ASYNC_PROVIDER_TYPES(V) #undef V - target->Set(FIXED_ONE_BYTE_STRING(isolate, "Providers"), async_providers); + target->ForceSet(context, + FIXED_ONE_BYTE_STRING(isolate, "Providers"), + async_providers, + ReadOnlyDontDelete).FromJust(); + + // Pass the async_wrap object to Environment::AsyncHooks so the + // async_id_stack property can be set automatically. + env->async_hooks()->set_async_wrap_object(target); env->set_async_hooks_init_function(Local()); - env->set_async_hooks_pre_function(Local()); - env->set_async_hooks_post_function(Local()); + env->set_async_hooks_before_function(Local()); + env->set_async_hooks_after_function(Local()); env->set_async_hooks_destroy_function(Local()); } +void AsyncWrap::GetAsyncId(const FunctionCallbackInfo& args) { + AsyncWrap* wrap; + args.GetReturnValue().Set(-1); + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + args.GetReturnValue().Set(wrap->get_id()); +} + + void AsyncWrap::DestroyIdsCb(uv_idle_t* handle) { uv_idle_stop(handle); Environment* env = Environment::from_destroy_ids_idle_handle(handle); - // None of the V8 calls done outside the HandleScope leak a handle. If this - // changes in the future then the SealHandleScope wrapping the uv_run() - // will catch this can cause the process to abort. + HandleScope handle_scope(env->isolate()); Context::Scope context_scope(env->context()); Local fn = env->async_hooks_destroy_function(); - if (fn.IsEmpty()) - return env->destroy_ids_list()->clear(); - TryCatch try_catch(env->isolate()); - std::vector destroy_ids_list; + std::vector destroy_ids_list; destroy_ids_list.swap(*env->destroy_ids_list()); for (auto current_id : destroy_ids_list) { // Want each callback to be cleaned up after itself, instead of cleaning @@ -219,6 +304,51 @@ void AsyncWrap::DestroyIdsCb(uv_idle_t* handle) { } +void AsyncWrap::AsyncReset(const FunctionCallbackInfo& args) { + AsyncWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + wrap->Reset(); +} + + +void AsyncWrap::AddIdToDestroyList(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + // This technically shouldn't be needed, since kDestroy should have been + // checked before calling this function. So make this a CHECK instead? + if (env->async_hooks()->fields()[AsyncHooks::kDestroy] == 0) { + return; + } + + CHECK(args[0]->IsNumber()); + double async_id = args[0]->NumberValue(); + CHECK_GT(async_id, 0); + + if (env->destroy_ids_list()->empty()) + uv_idle_start(env->destroy_ids_idle_handle(), DestroyIdsCb); + + env->destroy_ids_list()->push_back(async_id); +} + + +void AsyncWrap::GenIdArray(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + env->async_hooks()->gen_id_array(); +} + + +void AsyncWrap::TrimIdArray(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + env->async_hooks()->trim_id_array(); +} + + +void AsyncWrap::ResetIdArray(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + env->async_hooks()->reset_id_array(); +} + + void LoadAsyncWrapperInfo(Environment* env) { HeapProfiler* heap_profiler = env->isolate()->GetHeapProfiler(); #define V(PROVIDER) \ @@ -231,64 +361,63 @@ void LoadAsyncWrapperInfo(Environment* env) { AsyncWrap::AsyncWrap(Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent) - : BaseObject(env, object), bits_(static_cast(provider) << 1), - uid_(env->get_async_wrap_uid()) { + ProviderType provider) + : BaseObject(env, object), + provider_type_(provider) { CHECK_NE(provider, PROVIDER_NONE); CHECK_GE(object->InternalFieldCount(), 1); // Shift provider value over to prevent id collision. persistent().SetWrapperClassId(NODE_ASYNC_ID_OFFSET + provider); - Local init_fn = env->async_hooks_init_function(); + // Use ther Reset() call to call the init() callbacks. + Reset(); +} - // No init callback exists, no reason to go on. - if (init_fn.IsEmpty()) - return; - // If async wrap callbacks are disabled and no parent was passed that has - // run the init callback then return. - if (!env->async_wrap_callbacks_enabled() && - (parent == nullptr || !parent->ran_init_callback())) +AsyncWrap::~AsyncWrap() { + if (env()->async_hooks()->fields()[AsyncHooks::kDestroy] == 0) { return; + } - HandleScope scope(env->isolate()); - - Local argv[] = { - Number::New(env->isolate(), get_uid()), - Int32::New(env->isolate(), provider), - Null(env->isolate()), - Null(env->isolate()) - }; + if (env()->destroy_ids_list()->empty()) + uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb); - if (parent != nullptr) { - argv[2] = Number::New(env->isolate(), parent->get_uid()); - argv[3] = parent->object(); - } + env()->destroy_ids_list()->push_back(get_id()); +} - TryCatch try_catch(env->isolate()); - MaybeLocal ret = - init_fn->Call(env->context(), object, arraysize(argv), argv); +// Generalized call for both the constructor and for handles that are pooled +// and reused over their lifetime. This way a new uid can be assigned when +// the resource is pulled out of the pool and put back into use. +void AsyncWrap::Reset() { + AsyncHooks* async_hooks = env()->async_hooks(); + id_ = env()->new_async_id(); + trigger_id_ = env()->exchange_init_trigger_id(0); - if (ret.IsEmpty()) { - ClearFatalExceptionHandlers(env); - FatalException(env->isolate(), try_catch); + // Nothing to execute, so can continue normally. + if (async_hooks->fields()[AsyncHooks::kInit] == 0) { + return; } - bits_ |= 1; // ran_init_callback() is true now. -} - + HandleScope scope(env()->isolate()); + Local init_fn = env()->async_hooks_init_function(); -AsyncWrap::~AsyncWrap() { - if (!ran_init_callback()) - return; + Local argv[] = { + Number::New(env()->isolate(), get_id()), + env()->async_hooks()->provider_string(provider_type()), + object(), + Number::New(env()->isolate(), get_trigger_id()), + }; - if (env()->destroy_ids_list()->empty()) - uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb); + TryCatch try_catch(env()->isolate()); + MaybeLocal ret = init_fn->Call( + env()->context(), object(), arraysize(argv), argv); - env()->destroy_ids_list()->push_back(get_uid()); + if (ret.IsEmpty()) { + ClearFatalExceptionHandlers(env()); + FatalException(env()->isolate(), try_catch); + } } @@ -297,11 +426,10 @@ Local AsyncWrap::MakeCallback(const Local cb, Local* argv) { CHECK(env()->context() == env()->isolate()->GetCurrentContext()); - Local pre_fn = env()->async_hooks_pre_function(); - Local post_fn = env()->async_hooks_post_function(); - Local uid = Number::New(env()->isolate(), get_uid()); + AsyncHooks* async_hooks = env()->async_hooks(); Local context = object(); Local domain; + Local uid; bool has_domain = false; Environment::AsyncCallbackScope callback_scope(env()); @@ -326,9 +454,15 @@ Local AsyncWrap::MakeCallback(const Local cb, } } - if (ran_init_callback() && !pre_fn.IsEmpty()) { + // Want currentId() to return the correct value from the callbacks. + AsyncHooks::ExecScope exec_scope(env(), get_id(), get_trigger_id()); + + if (async_hooks->fields()[AsyncHooks::kBefore] > 0) { + uid = Number::New(env()->isolate(), get_id()); + Local fn = env()->async_hooks_before_function(); TryCatch try_catch(env()->isolate()); - MaybeLocal ar = pre_fn->Call(env()->context(), context, 1, &uid); + MaybeLocal ar = fn->Call( + env()->context(), Undefined(env()->isolate()), 1, &uid); if (ar.IsEmpty()) { ClearFatalExceptionHandlers(env()); FatalException(env()->isolate(), try_catch); @@ -336,14 +470,23 @@ Local AsyncWrap::MakeCallback(const Local cb, } } - Local ret = cb->Call(context, argc, argv); + // Finally... Get to running the user's callback. + MaybeLocal ret = cb->Call(env()->context(), context, argc, argv); - if (ran_init_callback() && !post_fn.IsEmpty()) { - Local did_throw = Boolean::New(env()->isolate(), ret.IsEmpty()); - Local vals[] = { uid, did_throw }; + Local ret_v; + if (!ret.ToLocal(&ret_v)) { + return Local(); + } + + // If the callback failed then the after() hooks will be called at the end + // of _fatalException(). + if (async_hooks->fields()[AsyncHooks::kAfter] > 0) { + if (uid.IsEmpty()) + uid = Number::New(env()->isolate(), get_id()); + Local fn = env()->async_hooks_after_function(); TryCatch try_catch(env()->isolate()); - MaybeLocal ar = - post_fn->Call(env()->context(), context, arraysize(vals), vals); + MaybeLocal ar = fn->Call( + env()->context(), Undefined(env()->isolate()), 1, &uid); if (ar.IsEmpty()) { ClearFatalExceptionHandlers(env()); FatalException(env()->isolate(), try_catch); @@ -351,9 +494,8 @@ Local AsyncWrap::MakeCallback(const Local cb, } } - if (ret.IsEmpty()) { - return ret; - } + // The execution scope of the id and trigger_id only go this far. + exec_scope.Dispose(); if (has_domain) { Local exit_v = domain->Get(env()->exit_string()); @@ -366,7 +508,7 @@ Local AsyncWrap::MakeCallback(const Local cb, } if (callback_scope.in_makecallback()) { - return ret; + return ret_v; } Environment::TickInfo* tick_info = env()->tick_info(); @@ -375,18 +517,29 @@ Local AsyncWrap::MakeCallback(const Local cb, env()->isolate()->RunMicrotasks(); } + // Make sure the stack unwound properly. If there are nested MakeCallback's + // then it should return early and not reach this code. + CHECK_EQ(env()->current_async_id(), 0); + CHECK_EQ(env()->trigger_id(), 0); + Local process = env()->process_object(); if (tick_info->length() == 0) { tick_info->set_index(0); - return ret; + return ret_v; } - if (env()->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) { - return Local(); - } + MaybeLocal rcheck = + env()->tick_callback_function()->Call(env()->context(), + process, + 0, + nullptr); + + // Make sure the stack unwound properly. + CHECK_EQ(env()->current_async_id(), 0); + CHECK_EQ(env()->trigger_id(), 0); - return ret; + return rcheck.IsEmpty() ? Local() : ret_v; } } // namespace node diff --git a/src/async-wrap.h b/src/async-wrap.h index d01c6ce9f9b724..57ce15c176a136 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -15,17 +15,20 @@ namespace node { #define NODE_ASYNC_PROVIDER_TYPES(V) \ V(NONE) \ - V(CRYPTO) \ + V(CONNECTION) \ V(FSEVENTWRAP) \ V(FSREQWRAP) \ V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ V(HTTPPARSER) \ V(JSSTREAM) \ - V(PIPEWRAP) \ + V(PBKDF2REQUEST) \ V(PIPECONNECTWRAP) \ + V(PIPEWRAP) \ V(PROCESSWRAP) \ V(QUERYWRAP) \ + V(RANDOMBYTESREQUEST) \ + V(SENDWRAP) \ V(SHUTDOWNWRAP) \ V(SIGNALWRAP) \ V(STATWATCHER) \ @@ -35,9 +38,8 @@ namespace node { V(TLSWRAP) \ V(TTYWRAP) \ V(UDPWRAP) \ - V(UDPSENDWRAP) \ V(WRITEWRAP) \ - V(ZLIB) + V(ZCTX) class Environment; @@ -48,12 +50,12 @@ class AsyncWrap : public BaseObject { PROVIDER_ ## PROVIDER, NODE_ASYNC_PROVIDER_TYPES(V) #undef V + PROVIDERS_LENGTH, }; AsyncWrap(Environment* env, v8::Local object, - ProviderType provider, - AsyncWrap* parent = nullptr); + ProviderType provider); virtual ~AsyncWrap(); @@ -61,34 +63,52 @@ class AsyncWrap : public BaseObject { v8::Local unused, v8::Local context); + static void GetAsyncId(const v8::FunctionCallbackInfo& args); + static void DestroyIdsCb(uv_idle_t* handle); + static void AsyncReset(const v8::FunctionCallbackInfo& args); + + static void AddIdToDestroyList( + const v8::FunctionCallbackInfo& args); + + // Counterpart to Environment::AsyncHooks::gen_id_array(). + static void GenIdArray(const v8::FunctionCallbackInfo& args); + + // Counterpart to Environment::AsyncHooks::trim_id_array(). + static void TrimIdArray(const v8::FunctionCallbackInfo& args); + + // Counterpart to Environment::AsyncHooks::reset_id_array(). + static void ResetIdArray(const v8::FunctionCallbackInfo& args); + inline ProviderType provider_type() const; - inline int64_t get_uid() const; + inline double get_id() const; + + inline double get_trigger_id() const; + + void Reset(); // Only call these within a valid HandleScope. + // TODO(trevnorris): These should return a MaybeLocal. v8::Local MakeCallback(const v8::Local cb, - int argc, - v8::Local* argv); + int argc, + v8::Local* argv); inline v8::Local MakeCallback(const v8::Local symbol, - int argc, - v8::Local* argv); + int argc, + v8::Local* argv); inline v8::Local MakeCallback(uint32_t index, - int argc, - v8::Local* argv); + int argc, + v8::Local* argv); virtual size_t self_size() const = 0; private: inline AsyncWrap(); - inline bool ran_init_callback() const; - - // When the async hooks init JS function is called from the constructor it is - // expected the context object will receive a _asyncQueue object property - // that will be used to call pre/post in MakeCallback. - uint32_t bits_; - const int64_t uid_; + const ProviderType provider_type_; + // Because the values may be Reset(), cannot be made const. + double id_; + double trigger_id_; }; void LoadAsyncWrapperInfo(Environment* env); diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 2b61209f6e643a..32c08b6bb708de 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -82,6 +82,7 @@ inline const char* ToErrorCodeString(int status) { class GetAddrInfoReqWrap : public ReqWrap { public: GetAddrInfoReqWrap(Environment* env, Local req_wrap_obj); + ~GetAddrInfoReqWrap(); size_t self_size() const override { return sizeof(*this); } }; @@ -93,6 +94,11 @@ GetAddrInfoReqWrap::GetAddrInfoReqWrap(Environment* env, } +GetAddrInfoReqWrap::~GetAddrInfoReqWrap() { + ClearWrap(object()); +} + + static void NewGetAddrInfoReqWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); } @@ -101,6 +107,7 @@ static void NewGetAddrInfoReqWrap(const FunctionCallbackInfo& args) { class GetNameInfoReqWrap : public ReqWrap { public: GetNameInfoReqWrap(Environment* env, Local req_wrap_obj); + ~GetNameInfoReqWrap(); size_t self_size() const override { return sizeof(*this); } }; @@ -112,6 +119,11 @@ GetNameInfoReqWrap::GetNameInfoReqWrap(Environment* env, } +GetNameInfoReqWrap::~GetNameInfoReqWrap() { + ClearWrap(object()); +} + + static void NewGetNameInfoReqWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); } @@ -286,6 +298,7 @@ class QueryWrap : public AsyncWrap { : AsyncWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_QUERYWRAP) { if (env->in_domain()) req_wrap_obj->Set(env->domain_string(), env->domain_array()->Get(0)); + Wrap(req_wrap_obj, this); } ~QueryWrap() override { @@ -1381,6 +1394,7 @@ static void Initialize(Local target, Local aiw = FunctionTemplate::New(env->isolate(), NewGetAddrInfoReqWrap); aiw->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(aiw, "getAsyncId", AsyncWrap::GetAsyncId); aiw->SetClassName( FIXED_ONE_BYTE_STRING(env->isolate(), "GetAddrInfoReqWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "GetAddrInfoReqWrap"), @@ -1389,6 +1403,7 @@ static void Initialize(Local target, Local niw = FunctionTemplate::New(env->isolate(), NewGetNameInfoReqWrap); niw->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(niw, "getAsyncId", AsyncWrap::GetAsyncId); niw->SetClassName( FIXED_ONE_BYTE_STRING(env->isolate(), "GetNameInfoReqWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "GetNameInfoReqWrap"), @@ -1397,6 +1412,7 @@ static void Initialize(Local target, Local qrw = FunctionTemplate::New(env->isolate(), NewQueryReqWrap); qrw->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(qrw, "getAsyncId", AsyncWrap::GetAsyncId); qrw->SetClassName( FIXED_ONE_BYTE_STRING(env->isolate(), "QueryReqWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "QueryReqWrap"), diff --git a/src/connect_wrap.cc b/src/connect_wrap.cc index df3f093e732972..e373b5a36e33e6 100644 --- a/src/connect_wrap.cc +++ b/src/connect_wrap.cc @@ -19,4 +19,9 @@ ConnectWrap::ConnectWrap(Environment* env, Wrap(req_wrap_obj, this); } + +ConnectWrap::~ConnectWrap() { + ClearWrap(object()); +} + } // namespace node diff --git a/src/connect_wrap.h b/src/connect_wrap.h index 28d4872d7ed416..7b16a5448745aa 100644 --- a/src/connect_wrap.h +++ b/src/connect_wrap.h @@ -15,6 +15,7 @@ class ConnectWrap : public ReqWrap { ConnectWrap(Environment* env, v8::Local req_wrap_obj, AsyncWrap::ProviderType provider); + ~ConnectWrap(); size_t self_size() const override { return sizeof(*this); } }; diff --git a/src/connection_wrap.cc b/src/connection_wrap.cc index 020fe8b4c9508c..30cf4bc11e6086 100644 --- a/src/connection_wrap.cc +++ b/src/connection_wrap.cc @@ -23,13 +23,11 @@ using v8::Value; template ConnectionWrap::ConnectionWrap(Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent) + ProviderType provider) : StreamWrap(env, object, reinterpret_cast(&handle_), - provider, - parent) {} + provider) {} template @@ -115,14 +113,12 @@ void ConnectionWrap::AfterConnect(uv_connect_t* req, template ConnectionWrap::ConnectionWrap( Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent); + ProviderType provider); template ConnectionWrap::ConnectionWrap( Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent); + ProviderType provider); template void ConnectionWrap::OnConnection( uv_stream_t* handle, int status); diff --git a/src/connection_wrap.h b/src/connection_wrap.h index 7af97fd3f05e1b..99fe5697ed91fa 100644 --- a/src/connection_wrap.h +++ b/src/connection_wrap.h @@ -22,8 +22,7 @@ class ConnectionWrap : public StreamWrap { protected: ConnectionWrap(Environment* env, v8::Local object, - ProviderType provider, - AsyncWrap* parent); + ProviderType provider); ~ConnectionWrap() { } diff --git a/src/env-inl.h b/src/env-inl.h index 1a17e2947d0597..c71fda6c235385 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -59,8 +59,34 @@ inline uint32_t* IsolateData::zero_fill_field() const { return zero_fill_field_; } -inline Environment::AsyncHooks::AsyncHooks() { - for (int i = 0; i < kFieldsCount; i++) fields_[i] = 0; +inline Environment::AsyncHooks::AsyncHooks(v8::Isolate* isolate) + : isolate_(isolate), + fields_(), + uid_fields_(), + id_stack_(new double[AsyncHooks::kIdStackLimit]()) { + v8::HandleScope handle_scope(isolate_); + // kAsyncUidCntr should start at 1 because that'll be the id for bootstrap. + uid_fields_[AsyncHooks::kAsyncUidCntr] = 1; +#define V(Provider) \ + providers_[AsyncWrap::PROVIDER_ ## Provider].Set( \ + isolate_, \ + v8::String::NewFromOneByte( \ + isolate_, \ + reinterpret_cast(#Provider), \ + v8::NewStringType::kInternalized, \ + sizeof(#Provider) - 1).ToLocalChecked()); + NODE_ASYNC_PROVIDER_TYPES(V) +#undef V + + v8::Local id_stack_ab = v8::ArrayBuffer::New( + isolate_, + id_stack_, + AsyncHooks::kIdStackLimit * sizeof(double)); + v8::Local id_stack_array = + v8::Float64Array::New(id_stack_ab, 0, AsyncHooks::kIdStackLimit); + v8::Persistent* p_float_array = + new v8::Persistent(isolate_, id_stack_array); + stack_of_id_stacks_.push(p_float_array); } inline uint32_t* Environment::AsyncHooks::fields() { @@ -71,12 +97,214 @@ inline int Environment::AsyncHooks::fields_count() const { return kFieldsCount; } -inline bool Environment::AsyncHooks::callbacks_enabled() { - return fields_[kEnableCallbacks] != 0; +inline double* Environment::AsyncHooks::uid_fields() { + return uid_fields_; +} + +inline int Environment::AsyncHooks::uid_fields_count() const { + return kUidFieldsCount; +} + +inline v8::Local Environment::AsyncHooks::provider_string(int idx) { + return providers_[idx].Get(isolate_); +} + +inline double* Environment::AsyncHooks::get_id_stack() { + return id_stack_; +} + +inline void Environment::AsyncHooks::push_to_id_stack(double id, + double trigger_id) { + CHECK_GE(id, 0); + CHECK_GE(trigger_id, 0); + // First make sure that JS hasn't totally screwed up the stack. + CHECK_LE(fields_[AsyncHooks::kIdStackIndex], AsyncHooks::kIdStackLimit - 2); + // Also verify that Size and Index are in sync. + CHECK_EQ(fields_[AsyncHooks::kIdStackSize] % AsyncHooks::kIdStackLimit, + fields_[AsyncHooks::kIdStackIndex]); + + fields_[AsyncHooks::kIdStackIndex] += 2; + fields_[AsyncHooks::kIdStackSize] += 2; + + if (fields_[AsyncHooks::kIdStackIndex] == AsyncHooks::kIdStackLimit) { + gen_id_array(); + + // Final check to make sure we're writing to the right spot in memory. + CHECK_LE(fields_[AsyncHooks::kIdStackIndex], AsyncHooks::kIdStackLimit - 2); + } + + id_stack_[fields_[AsyncHooks::kIdStackIndex]] = id; + id_stack_[fields_[AsyncHooks::kIdStackIndex] + 1] = trigger_id; +} + +inline void Environment::AsyncHooks::pop_from_id_stack(double id) { + // In case of a fatal exception then this may have already been reset, + // if the stack was multiple MakeCallback()'s deep. + if (fields_[AsyncHooks::kIdStackSize] == 0) { + // Sanity check to make sure the id stack hasn't become corrupted. + CHECK_EQ(fields_[AsyncHooks::kIdStackIndex], 0); + return; + } + + // Make sure the stack hasn't become corrupted. + if (id_stack_[fields_[AsyncHooks::kIdStackIndex]] != id) { + fprintf(stderr, + "Error: async hook stack has become corrupted (" + "actual: %'.f, expected: %'.f)\n", + id_stack_[fields_[AsyncHooks::kIdStackIndex]], + id); + Environment* env = Environment::GetCurrent(isolate_); + DumpBacktrace(stderr); + fflush(stderr); + if (!env->abort_on_uncaught_exception()) + exit(1); + fprintf(stderr, "\n"); + fflush(stderr); + ABORT_NO_BACKTRACE(); + } + + // Fast path where there's probably no extra stack. + if (fields_[AsyncHooks::kIdStackSize] < AsyncHooks::kIdStackLimit) { + CHECK_GE(fields_[AsyncHooks::kIdStackIndex], 2); + CHECK_EQ(fields_[AsyncHooks::kIdStackIndex], + fields_[AsyncHooks::kIdStackSize]); + fields_[AsyncHooks::kIdStackIndex] -= 2; + fields_[AsyncHooks::kIdStackSize] -= 2; + return; + } + + if (fields_[AsyncHooks::kIdStackIndex] == 0) { + trim_id_array(); + } + + CHECK_EQ(fields_[AsyncHooks::kIdStackSize] % AsyncHooks::kIdStackLimit, + fields_[AsyncHooks::kIdStackIndex]); + + fields_[AsyncHooks::kIdStackIndex] -= 2; + fields_[AsyncHooks::kIdStackSize] -= 2; +} + +inline void Environment::AsyncHooks::gen_id_array() { + // This should only be called when there are enough ids to merit creating + // another id array. + CHECK_EQ(fields_[AsyncHooks::kIdStackIndex], AsyncHooks::kIdStackLimit); + + v8::HandleScope handle_scope(isolate_); + id_stack_ = new double[AsyncHooks::kIdStackLimit](); + v8::Local id_stack_ab = v8::ArrayBuffer::New( + isolate_, + id_stack_, + AsyncHooks::kIdStackLimit * sizeof(double)); + v8::Local id_stack_array = + v8::Float64Array::New(id_stack_ab, 0, AsyncHooks::kIdStackLimit); + v8::Persistent* p_array = + new v8::Persistent(isolate_, id_stack_array); + stack_of_id_stacks_.push(p_array); + + fields_[AsyncHooks::kIdStackIndex] = 0; + + // If we happen to need a new stack before the async_wrap object has been set + // then return early. + if (async_wrap_object_.IsEmpty()) + return; + + async_wrap_object_.Get(isolate_)->Set( + isolate_->GetCurrentContext(), + FIXED_ONE_BYTE_STRING(isolate_, "async_id_stack"), + StrongPersistentToLocal(*(stack_of_id_stacks_.top()))).FromJust(); +} + +inline void Environment::AsyncHooks::trim_id_array() { + // This shouldn't be called if there's only one id stack left. + CHECK_GT(stack_of_id_stacks_.size(), 1); + CHECK_EQ(fields_[AsyncHooks::kIdStackIndex], 0); + CHECK_GE(fields_[AsyncHooks::kIdStackSize], AsyncHooks::kIdStackLimit - 2); + + // Proper cleanup of the previous Persistent + v8::HandleScope handle_scope(isolate_); + v8::Persistent* p_array = stack_of_id_stacks_.top(); + v8::Local ab = StrongPersistentToLocal(*p_array)->Buffer(); + double* data = reinterpret_cast(ab->GetContents().Data()); + stack_of_id_stacks_.pop(); + p_array->Reset(); + ab->Neuter(); + delete[] data; + delete p_array; + + // Reset id_stack_ to old Float64Array. + p_array = stack_of_id_stacks_.top(); + ab = StrongPersistentToLocal(*p_array)->Buffer(); + id_stack_ = reinterpret_cast(ab->GetContents().Data()); + + fields_[AsyncHooks::kIdStackIndex] = AsyncHooks::kIdStackLimit - 2; + + CHECK_EQ(fields_[AsyncHooks::kIdStackSize] % AsyncHooks::kIdStackLimit, + fields_[AsyncHooks::kIdStackIndex]); + + // If we happen to need a new stack before the async_wrap object has been set + // then return early. + if (async_wrap_object_.IsEmpty()) + return; + + async_wrap_object_.Get(isolate_)->Set( + isolate_->GetCurrentContext(), + FIXED_ONE_BYTE_STRING(isolate_, "async_id_stack"), + StrongPersistentToLocal(*p_array)).FromJust(); +} + +inline void Environment::AsyncHooks::reset_id_array() { + v8::HandleScope handle_scope(isolate_); + + while (stack_of_id_stacks_.size() > 1) { + v8::Persistent* p_array = stack_of_id_stacks_.top(); + v8::Local ab = StrongPersistentToLocal(*p_array)->Buffer(); + double* data = reinterpret_cast(ab->GetContents().Data()); + stack_of_id_stacks_.pop(); + p_array->Reset(); + ab->Neuter(); + delete[] data; + delete p_array; + } + + v8::Persistent* p_array = stack_of_id_stacks_.top(); + v8::Local ab = StrongPersistentToLocal(*p_array)->Buffer(); + id_stack_ = reinterpret_cast(ab->GetContents().Data()); + + fields_[AsyncHooks::kIdStackIndex] = 0; + fields_[AsyncHooks::kIdStackSize] = 0; +} + +inline void Environment::AsyncHooks::set_async_wrap_object( + v8::Local object) { + // This should only be set once. + CHECK(async_wrap_object_.IsEmpty()); + + v8::HandleScope handle_scope(isolate_); + async_wrap_object_.Set(isolate_, object); + object->Set(isolate_->GetCurrentContext(), + FIXED_ONE_BYTE_STRING(isolate_, "async_id_stack"), + StrongPersistentToLocal(*(stack_of_id_stacks_.top()))).FromJust(); +} + +inline Environment::AsyncHooks::ExecScope::ExecScope( + Environment* env, double id, double trigger_id) + : env_(env), + id_(id), + disposed_(false) { + env->async_hooks()->push_to_id_stack(id, trigger_id); + + auto fields = env->async_hooks()->fields(); + CHECK_EQ(fields[AsyncHooks::kIdStackSize] % AsyncHooks::kIdStackLimit, + fields[AsyncHooks::kIdStackIndex]); } -inline void Environment::AsyncHooks::set_enable_callbacks(uint32_t flag) { - fields_[kEnableCallbacks] = flag; +inline void Environment::AsyncHooks::ExecScope::Dispose() { + disposed_ = true; + env_->async_hooks()->pop_from_id_stack(id_); + + auto fields = env_->async_hooks()->fields(); + CHECK_EQ(fields[AsyncHooks::kIdStackSize] % AsyncHooks::kIdStackLimit, + fields[AsyncHooks::kIdStackIndex]); } inline Environment::AsyncCallbackScope::AsyncCallbackScope(Environment* env) @@ -166,12 +394,14 @@ inline Environment::Environment(IsolateData* isolate_data, v8::Local context) : isolate_(context->GetIsolate()), isolate_data_(isolate_data), + async_hooks_(context->GetIsolate()), timer_base_(uv_now(isolate_data->event_loop())), using_domains_(false), printed_error_(false), trace_sync_io_(false), + abort_on_uncaught_exception_(false), makecallback_cntr_(0), - async_wrap_uid_(0), + fd_async_ids_inst_(), debugger_agent_(this), #if HAVE_INSPECTOR inspector_agent_(this), @@ -190,7 +420,6 @@ inline Environment::Environment(IsolateData* isolate_data, fn->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "InternalFieldObject")); v8::Local obj = fn->InstanceTemplate(); obj->SetInternalFieldCount(1); - set_generic_internal_field_template(obj); RB_INIT(&cares_task_list_); AssignToContext(context); @@ -238,11 +467,6 @@ inline v8::Isolate* Environment::isolate() const { return isolate_; } -inline bool Environment::async_wrap_callbacks_enabled() const { - // The const_cast is okay, it doesn't violate conceptual const-ness. - return const_cast(this)->async_hooks()->callbacks_enabled(); -} - inline bool Environment::in_domain() const { // The const_cast is okay, it doesn't violate conceptual const-ness. return using_domains() && @@ -321,14 +545,63 @@ inline void Environment::set_trace_sync_io(bool value) { trace_sync_io_ = value; } -inline int64_t Environment::get_async_wrap_uid() { - return ++async_wrap_uid_; +inline bool Environment::abort_on_uncaught_exception() const { + return abort_on_uncaught_exception_; } -inline std::vector* Environment::destroy_ids_list() { +inline void Environment::set_abort_on_uncaught_exception(bool value) { + abort_on_uncaught_exception_ = value; +} + +inline std::vector* Environment::destroy_ids_list() { return &destroy_ids_list_; } +inline double Environment::new_async_id() { + return ++async_hooks()->uid_fields()[AsyncHooks::kAsyncUidCntr]; +} + +inline double Environment::current_async_id() { + return async_hooks()->get_id_stack()[ + async_hooks()->fields()[AsyncHooks::kIdStackIndex]]; +} + +inline double Environment::trigger_id() { + return async_hooks()->get_id_stack()[ + async_hooks()->fields()[AsyncHooks::kIdStackIndex] + 1]; +} + +inline double Environment::exchange_init_trigger_id(const double id) { + auto uid_fields = async_hooks()->uid_fields(); + double tid = uid_fields[AsyncHooks::kInitTriggerId]; + uid_fields[AsyncHooks::kInitTriggerId] = id; + if (tid <= 0) tid = uid_fields[AsyncHooks::kScopedTriggerId]; + if (tid <= 0) tid = current_async_id(); + return tid; +} + +inline void Environment::set_init_trigger_id(const double id) { + async_hooks()->uid_fields()[AsyncHooks::kInitTriggerId] = id; +} + +inline void Environment::erase_fd_async_id(int fd) { + fd_async_id_map_.erase(fd); +} + +inline node_fd_async_ids Environment::get_fd_async_id(int fd) { + return fd_async_id_map_[fd]; +} + +inline void Environment::insert_fd_async_ids(int fd, + double async_id, + double trigger_id) { + fd_async_id_map_[fd] = { async_id, trigger_id }; +} + +inline node_fd_async_ids* Environment::get_fd_async_ids_inst() { + return &fd_async_ids_inst_; +} + inline double* Environment::heap_statistics_buffer() const { CHECK_NE(heap_statistics_buffer_, nullptr); return heap_statistics_buffer_; @@ -465,12 +738,6 @@ inline void Environment::SetTemplateMethod(v8::Local that, t->SetClassName(name_string); // NODE_SET_METHOD() compatibility. } -inline v8::Local Environment::NewInternalFieldObject() { - v8::MaybeLocal m_obj = - generic_internal_field_template()->NewInstance(context()); - return m_obj.ToLocalChecked(); -} - #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ diff --git a/src/env.h b/src/env.h index 8c256ca9c7faa4..f59d5da0858857 100644 --- a/src/env.h +++ b/src/env.h @@ -17,6 +17,8 @@ #include #include +#include +#include // Caveat emptor: we're going slightly crazy with macros here but the end // hopefully justifies the means. We have a lot of per-context properties @@ -69,7 +71,6 @@ namespace node { V(address_string, "address") \ V(args_string, "args") \ V(async, "async") \ - V(async_queue_string, "_asyncQueue") \ V(buffer_string, "buffer") \ V(bytes_string, "bytes") \ V(bytes_parsed_string, "bytesParsed") \ @@ -231,8 +232,9 @@ namespace node { V(as_external, v8::External) \ V(async_hooks_destroy_function, v8::Function) \ V(async_hooks_init_function, v8::Function) \ - V(async_hooks_post_function, v8::Function) \ - V(async_hooks_pre_function, v8::Function) \ + V(async_hooks_before_function, v8::Function) \ + V(async_hooks_fatal_error_function, v8::Function) \ + V(async_hooks_after_function, v8::Function) \ V(binding_cache_object, v8::Object) \ V(buffer_constructor_function, v8::Function) \ V(buffer_prototype_object, v8::Object) \ @@ -240,13 +242,14 @@ namespace node { V(domain_array, v8::Array) \ V(domains_stack_array, v8::Array) \ V(fs_stats_constructor_function, v8::Function) \ - V(generic_internal_field_template, v8::ObjectTemplate) \ V(jsstream_constructor_template, v8::FunctionTemplate) \ V(module_load_list_array, v8::Array) \ + V(pbkdf2_constructor_template, v8::ObjectTemplate) \ V(pipe_constructor_template, v8::FunctionTemplate) \ V(process_object, v8::Object) \ V(promise_reject_function, v8::Function) \ V(push_values_to_array_function, v8::Function) \ + V(randombytes_constructor_template, v8::ObjectTemplate) \ V(script_context_constructor_template, v8::FunctionTemplate) \ V(script_data_constructor_function, v8::Function) \ V(secure_context_constructor_template, v8::FunctionTemplate) \ @@ -267,6 +270,11 @@ struct node_ares_task { RB_ENTRY(node_ares_task) node; }; +struct node_fd_async_ids { + double async_id; + double trigger_id; +}; + RB_HEAD(node_ares_task_list, node_ares_task); class IsolateData { @@ -307,22 +315,102 @@ class Environment { public: class AsyncHooks { public: + // Reason for both UidFields and Fields are that one is stored as a double* + // and the other as a uint32_t*. + enum UidFields { + kAsyncUidCntr, + kInitTriggerId, + kScopedTriggerId, + kUidFieldsCount, + }; + + enum Fields { + kInit, + kBefore, + kAfter, + kDestroy, + kActiveHooks, + kIdStackIndex, + kIdStackSize, + kFieldsCount, + }; + + static const size_t kIdStackLimit = 1024; + inline uint32_t* fields(); inline int fields_count() const; - inline bool callbacks_enabled(); - inline void set_enable_callbacks(uint32_t flag); + inline double* uid_fields(); + inline int uid_fields_count() const; + inline v8::Local provider_string(int idx); + + inline double* get_id_stack(); + inline void push_to_id_stack(double id, double trigger_id); + inline void pop_from_id_stack(double id); + inline void gen_id_array(); + inline void trim_id_array(); + inline void reset_id_array(); // Used in fatal exceptions. + inline void set_async_wrap_object(v8::Local object); + + class InitScope { + public: + explicit InitScope(Environment* env, double init_trigger_id) + : uid_fields_(env->async_hooks()->uid_fields()), + init_trigger_id_(uid_fields_[AsyncHooks::kScopedTriggerId]) { + uid_fields_[AsyncHooks::kScopedTriggerId] = init_trigger_id; + } + ~InitScope() { + uid_fields_[AsyncHooks::kScopedTriggerId] = init_trigger_id_; + } + private: + double* uid_fields_; + const double init_trigger_id_; + + DISALLOW_COPY_AND_ASSIGN(InitScope); + }; + + // ExecScope is meant for use in MakeCallback, to manage the id stack. + class ExecScope { + public: + explicit ExecScope(Environment* env, double id, double trigger_id); + ~ExecScope() { + if (disposed_) return; + Dispose(); + } + void Dispose(); + + private: + ExecScope() { } + Environment* env_; + double id_; + // Manually track if disposed so if the user calls Dispose() and it's + // RAII it won't alter the id stack twice. + bool disposed_; + + DISALLOW_COPY_AND_ASSIGN(ExecScope); + }; private: friend class Environment; // So we can call the constructor. inline AsyncHooks(); + inline explicit AsyncHooks(v8::Isolate* isolate); - enum Fields { - // Set this to not zero if the init hook should be called. - kEnableCallbacks, - kFieldsCount - }; + // Keep a list of all Persistent strings used for Provider types. + v8::Eternal providers_[AsyncWrap::PROVIDERS_LENGTH]; + // Store a reference to the async_wrap object so we can override the + // async_id_stack property if the stack grows too large. + v8::Eternal async_wrap_object_; + + std::stack*> stack_of_id_stacks_; + + v8::Isolate* isolate_; uint32_t fields_[kFieldsCount]; + // Gives us 2^53-1 unique ids. Good enough for now and makes the operation + // cheaper in JS. + double uid_fields_[kUidFieldsCount]; + // Pointer to the data in the Float64Array that holds the current stack of + // ids. + double* id_stack_; DISALLOW_COPY_AND_ASSIGN(AsyncHooks); }; @@ -427,7 +515,6 @@ class Environment { inline v8::Isolate* isolate() const; inline uv_loop_t* event_loop() const; - inline bool async_wrap_callbacks_enabled() const; inline bool in_domain() const; inline uint32_t watched_providers() const; @@ -464,10 +551,24 @@ class Environment { void PrintSyncTrace() const; inline void set_trace_sync_io(bool value); - inline int64_t get_async_wrap_uid(); + inline bool abort_on_uncaught_exception() const; + inline void set_abort_on_uncaught_exception(bool value); + + // The necessary API for async_hooks. + inline double new_async_id(); + inline double current_async_id(); + inline double trigger_id(); + inline double exchange_init_trigger_id(const double id); + inline void set_init_trigger_id(const double id); + + // For propagating hook id's with a file descriptor. + inline void erase_fd_async_id(int fd); + inline node_fd_async_ids get_fd_async_id(int fd); + inline void insert_fd_async_ids(int fd, double async_id, double trigger_id); + inline node_fd_async_ids* get_fd_async_ids_inst(); // List of id's that have been destroyed and need the destroy() cb called. - inline std::vector* destroy_ids_list(); + inline std::vector* destroy_ids_list(); inline double* heap_statistics_buffer() const; inline void set_heap_statistics_buffer(double* pointer); @@ -507,8 +608,6 @@ class Environment { const char* name, v8::FunctionCallback callback); - inline v8::Local NewInternalFieldObject(); - // Strings and private symbols are shared across shared contexts // The getters simply proxy to the per-isolate primitive. #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) @@ -567,9 +666,12 @@ class Environment { bool using_domains_; bool printed_error_; bool trace_sync_io_; + bool abort_on_uncaught_exception_; size_t makecallback_cntr_; - int64_t async_wrap_uid_; - std::vector destroy_ids_list_; + double async_wrap_id_; + std::vector destroy_ids_list_; + node_fd_async_ids fd_async_ids_inst_; + std::unordered_map fd_async_id_map_; debugger::Agent debugger_agent_; #if HAVE_INSPECTOR inspector::Agent inspector_agent_; diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc index 025f511d93b1a5..80038dea4f649c 100644 --- a/src/fs_event_wrap.cc +++ b/src/fs_event_wrap.cc @@ -70,6 +70,7 @@ void FSEventWrap::Initialize(Local target, t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(fsevent_string); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "start", Start); env->SetProtoMethod(t, "close", Close); diff --git a/src/handle_wrap.cc b/src/handle_wrap.cc index 317fb48b1d28e3..6c25d0eccbae24 100644 --- a/src/handle_wrap.cc +++ b/src/handle_wrap.cc @@ -69,9 +69,8 @@ void HandleWrap::Close(const FunctionCallbackInfo& args) { HandleWrap::HandleWrap(Environment* env, Local object, uv_handle_t* handle, - AsyncWrap::ProviderType provider, - AsyncWrap* parent) - : AsyncWrap(env, object, provider, parent), + AsyncWrap::ProviderType provider) + : AsyncWrap(env, object, provider), state_(kInitialized), handle_(handle) { handle_->data = this; diff --git a/src/handle_wrap.h b/src/handle_wrap.h index 2a128dd8b1679d..da69a44bff7bdb 100644 --- a/src/handle_wrap.h +++ b/src/handle_wrap.h @@ -53,8 +53,7 @@ class HandleWrap : public AsyncWrap { HandleWrap(Environment* env, v8::Local object, uv_handle_t* handle, - AsyncWrap::ProviderType provider, - AsyncWrap* parent = nullptr); + AsyncWrap::ProviderType provider); ~HandleWrap() override; private: diff --git a/src/js_stream.cc b/src/js_stream.cc index e51c4ae9b35084..2644a6a451a00f 100644 --- a/src/js_stream.cc +++ b/src/js_stream.cc @@ -12,7 +12,6 @@ namespace node { using v8::Array; using v8::Context; -using v8::External; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; @@ -21,8 +20,8 @@ using v8::Object; using v8::Value; -JSStream::JSStream(Environment* env, Local obj, AsyncWrap* parent) - : AsyncWrap(env, obj, AsyncWrap::PROVIDER_JSSTREAM, parent), +JSStream::JSStream(Environment* env, Local obj) + : AsyncWrap(env, obj, AsyncWrap::PROVIDER_JSSTREAM), StreamBase(env) { node::Wrap(obj, this); MakeWeak(this); @@ -115,17 +114,7 @@ void JSStream::New(const FunctionCallbackInfo& args) { // normal function. CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - JSStream* wrap; - - if (args.Length() == 0) { - wrap = new JSStream(env, args.This(), nullptr); - } else if (args[0]->IsExternal()) { - void* ptr = args[0].As()->Value(); - wrap = new JSStream(env, args.This(), static_cast(ptr)); - } else { - UNREACHABLE(); - } - CHECK(wrap); + new JSStream(env, args.This()); } @@ -221,6 +210,8 @@ void JSStream::Initialize(Local target, t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "JSStream")); t->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "doAlloc", DoAlloc); env->SetProtoMethod(t, "doRead", DoRead); env->SetProtoMethod(t, "doAfterWrite", DoAfterWrite); diff --git a/src/js_stream.h b/src/js_stream.h index 5a1244bc463e36..fc0b7abe15a633 100644 --- a/src/js_stream.h +++ b/src/js_stream.h @@ -33,7 +33,7 @@ class JSStream : public AsyncWrap, public StreamBase { size_t self_size() const override { return sizeof(*this); } protected: - JSStream(Environment* env, v8::Local obj, AsyncWrap* parent); + JSStream(Environment* env, v8::Local obj); AsyncWrap* GetAsyncWrap() override; diff --git a/src/node.cc b/src/node.cc index 57293989029331..393dc1aeb2d56b 100644 --- a/src/node.cc +++ b/src/node.cc @@ -132,6 +132,8 @@ using v8::Uint32Array; using v8::V8; using v8::Value; +using AsyncHooks = node::Environment::AsyncHooks; + static bool print_eval = false; static bool force_repl = false; static bool syntax_check_only = false; @@ -153,6 +155,7 @@ static node_module* modlist_linked; static node_module* modlist_addon; static bool trace_enabled = false; static const char* trace_enabled_categories = nullptr; +static bool abort_on_uncaught_exception = false; #if defined(NODE_HAVE_I18N_SUPPORT) // Path to ICU data (for i18n / Intl) @@ -1179,21 +1182,13 @@ Local MakeCallback(Environment* env, // If you hit this assertion, you forgot to enter the v8::Context first. CHECK_EQ(env->context(), env->isolate()->GetCurrentContext()); - Local pre_fn = env->async_hooks_pre_function(); - Local post_fn = env->async_hooks_post_function(); Local object, domain; - bool ran_init_callback = false; bool has_domain = false; Environment::AsyncCallbackScope callback_scope(env); - // TODO(trevnorris): Adding "_asyncQueue" to the "this" in the init callback - // is a horrible way to detect usage. Rethink how detection should happen. if (recv->IsObject()) { object = recv.As(); - Local async_queue_v = object->Get(env->async_queue_string()); - if (async_queue_v->IsObject()) - ran_init_callback = true; } if (env->using_domains()) { @@ -1217,34 +1212,12 @@ Local MakeCallback(Environment* env, } } - if (ran_init_callback && !pre_fn.IsEmpty()) { - TryCatch try_catch(env->isolate()); - MaybeLocal ar = pre_fn->Call(env->context(), object, 0, nullptr); - if (ar.IsEmpty()) { - ClearFatalExceptionHandlers(env); - FatalException(env->isolate(), try_catch); - return Local(); - } - } + // TODO(trevnorris): Correct this once node::MakeCallback() support id and + // triggerId. + AsyncHooks::ExecScope exec_scope(env, 0, 0); Local ret = callback->Call(recv, argc, argv); - if (ran_init_callback && !post_fn.IsEmpty()) { - Local did_throw = Boolean::New(env->isolate(), ret.IsEmpty()); - // Currently there's no way to retrieve an uid from node::MakeCallback(). - // This needs to be fixed. - Local vals[] = - { Undefined(env->isolate()).As(), did_throw }; - TryCatch try_catch(env->isolate()); - MaybeLocal ar = - post_fn->Call(env->context(), object, arraysize(vals), vals); - if (ar.IsEmpty()) { - ClearFatalExceptionHandlers(env); - FatalException(env->isolate(), try_catch); - return Local(); - } - } - if (ret.IsEmpty()) { // NOTE: For backwards compatibility with public API we return Undefined() // if the top level call threw. @@ -1252,6 +1225,8 @@ Local MakeCallback(Environment* env, ret : Undefined(env->isolate()).As(); } + exec_scope.Dispose(); + if (has_domain) { Local exit_v = domain->Get(env->exit_string()); if (exit_v->IsFunction()) { @@ -1272,6 +1247,11 @@ Local MakeCallback(Environment* env, env->isolate()->RunMicrotasks(); } + // Make sure the stack unwound properly. If there are nested MakeCallback's + // then it should return early and not reach this code. + CHECK_EQ(env->current_async_id(), 0); + CHECK_EQ(env->trigger_id(), 0); + Local process = env->process_object(); if (tick_info->length() == 0) { @@ -1288,10 +1268,10 @@ Local MakeCallback(Environment* env, Local MakeCallback(Environment* env, - Local recv, - Local symbol, - int argc, - Local argv[]) { + Local recv, + Local symbol, + int argc, + Local argv[]) { Local cb_v = recv->Get(symbol); CHECK(cb_v->IsFunction()); return MakeCallback(env, recv.As(), cb_v.As(), argc, argv); @@ -1299,10 +1279,10 @@ Local MakeCallback(Environment* env, Local MakeCallback(Environment* env, - Local recv, - const char* method, - int argc, - Local argv[]) { + Local recv, + const char* method, + int argc, + Local argv[]) { Local method_string = OneByteString(env->isolate(), method); return MakeCallback(env, recv, method_string, argc, argv); } @@ -3719,6 +3699,9 @@ static void ParseArgs(int* argc, } else if (strcmp(arg, "--expose-internals") == 0 || strcmp(arg, "--expose_internals") == 0) { // consumed in js + } else if (strcmp(arg, "--abort-on-uncaught-exception") || + strcmp(arg, "--abort_on_uncaught_exception")) { + abort_on_uncaught_exception = true; } else if (strcmp(arg, "--") == 0) { index += 1; break; @@ -4392,8 +4375,11 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data, return 12; // Signal internal error. } + env.set_abort_on_uncaught_exception(abort_on_uncaught_exception); + { Environment::AsyncCallbackScope callback_scope(&env); + Environment::AsyncHooks::ExecScope exec_scope(&env, 1, 0); LoadEnvironment(&env); } diff --git a/src/node_crypto.cc b/src/node_crypto.cc index f84d42f2321ab9..c62de0aabf9421 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -78,6 +78,7 @@ using v8::Isolate; using v8::Local; using v8::Null; using v8::Object; +using v8::ObjectTemplate; using v8::Persistent; using v8::PropertyAttribute; using v8::PropertyCallbackInfo; @@ -2718,6 +2719,7 @@ void Connection::Initialize(Environment* env, Local target) { t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Connection")); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "encIn", Connection::EncIn); env->SetProtoMethod(t, "clearOut", Connection::ClearOut); env->SetProtoMethod(t, "clearIn", Connection::ClearIn); @@ -5163,7 +5165,7 @@ class PBKDF2Request : public AsyncWrap { char* salt, int iter, int keylen) - : AsyncWrap(env, object, AsyncWrap::PROVIDER_CRYPTO), + : AsyncWrap(env, object, AsyncWrap::PROVIDER_PBKDF2REQUEST), digest_(digest), error_(0), passlen_(passlen), @@ -5382,7 +5384,8 @@ void PBKDF2(const FunctionCallbackInfo& args) { digest = EVP_sha1(); } - obj = env->NewInternalFieldObject(); + obj = env->pbkdf2_constructor_template()-> + NewInstance(env->context()).ToLocalChecked(); req = new PBKDF2Request(env, obj, digest, @@ -5428,7 +5431,7 @@ void PBKDF2(const FunctionCallbackInfo& args) { class RandomBytesRequest : public AsyncWrap { public: RandomBytesRequest(Environment* env, Local object, size_t size) - : AsyncWrap(env, object, AsyncWrap::PROVIDER_CRYPTO), + : AsyncWrap(env, object, AsyncWrap::PROVIDER_RANDOMBYTESREQUEST), error_(0), size_(size), data_(node::Malloc(size)) { @@ -5550,7 +5553,8 @@ void RandomBytes(const FunctionCallbackInfo& args) { if (size < 0 || size > Buffer::kMaxLength) return env->ThrowRangeError("size is not a valid Smi"); - Local obj = env->NewInternalFieldObject(); + Local obj = env->randombytes_constructor_template()-> + NewInstance(env->context()).ToLocalChecked(); RandomBytesRequest* req = new RandomBytesRequest(env, obj, size); if (args[1]->IsFunction()) { @@ -6019,6 +6023,20 @@ void InitCrypto(Local target, PublicKeyCipher::Cipher); + + Local pb = FunctionTemplate::New(env->isolate()); + pb->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PBKDF2")); + env->SetProtoMethod(pb, "getAsyncId", AsyncWrap::GetAsyncId); + Local pbt = pb->InstanceTemplate(); + pbt->SetInternalFieldCount(1); + env->set_pbkdf2_constructor_template(pbt); + + Local rb = FunctionTemplate::New(env->isolate()); + rb->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "RandomBytes")); + env->SetProtoMethod(rb, "getAsyncId", AsyncWrap::GetAsyncId); + Local rbt = rb->InstanceTemplate(); + rbt->SetInternalFieldCount(1); + env->set_randombytes_constructor_template(rbt); } } // namespace crypto diff --git a/src/node_crypto.h b/src/node_crypto.h index 175206c40df586..4c524e66181441 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -381,7 +381,7 @@ class Connection : public AsyncWrap, public SSLWrap { v8::Local wrap, SecureContext* sc, SSLWrap::Kind kind) - : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_CRYPTO), + : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_CONNECTION), SSLWrap(env, sc, kind), bio_read_(nullptr), bio_write_(nullptr), diff --git a/src/node_file.cc b/src/node_file.cc index 0abb88088786ae..23b029aee84e08 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -27,8 +27,10 @@ namespace node { using v8::Array; +using v8::ArrayBuffer; using v8::Context; using v8::EscapableHandleScope; +using v8::Float64Array; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -87,7 +89,10 @@ class FSReqWrap: public ReqWrap { Wrap(object(), this); } - ~FSReqWrap() { ReleaseEarly(); } + ~FSReqWrap() { + ReleaseEarly(); + ClearWrap(object()); + } void* operator new(size_t size) = delete; void* operator new(size_t size, char* storage) { return storage; } @@ -194,6 +199,9 @@ static void After(uv_fs_t *req) { break; case UV_FS_OPEN: + env->insert_fd_async_ids(req->result, + req_wrap->get_id(), + req_wrap->get_trigger_id()); argv[1] = Integer::New(env->isolate(), req->result); break; @@ -409,6 +417,9 @@ static void Close(const FunctionCallbackInfo& args) { return TYPE_ERROR("fd must be a file descriptor"); int fd = args[0]->Int32Value(); + // Can remove the async_id here because it's only used from JS when setting + // the init trigger id. + env->erase_fd_async_id(fd); if (args[1]->IsObject()) { ASYNC_CALL(close, args[1], UTF8, fd) @@ -1433,6 +1444,22 @@ static void Mkdtemp(const FunctionCallbackInfo& args) { } } + +// The asyncId and triggerId will be written to both indexes of the +// Float64Array stored on Environment::AsyncHooks. This is much faster than +// creating and returning an Array. +static void GetIdsFromFd(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args[0]->IsInt32()) + return TYPE_ERROR("fd must be an int"); + + auto ptr = env->get_fd_async_ids_inst(); + node_fd_async_ids slot = env->get_fd_async_id(args[0]->Int32Value()); + ptr->async_id = slot.async_id; + ptr->trigger_id = slot.trigger_id; +} + + void FSInitialize(const FunctionCallbackInfo& args) { Local stats_constructor = args[0].As(); CHECK(stats_constructor->IsFunction()); @@ -1441,6 +1468,7 @@ void FSInitialize(const FunctionCallbackInfo& args) { env->set_fs_stats_constructor_function(stats_constructor); } + void InitFs(Local target, Local unused, Local context, @@ -1489,12 +1517,28 @@ void InitFs(Local target, env->SetMethod(target, "mkdtemp", Mkdtemp); + env->SetMethod(target, "getIdsFromFd", GetIdsFromFd); + + // Set Float64Array used by GetIdsFromFd() to quickly retrieve an asyncId + // from the unordered_map. + node_fd_async_ids* fd_ids_inst = env->get_fd_async_ids_inst(); + Local ab = + ArrayBuffer::New(env->isolate(), + reinterpret_cast(fd_ids_inst), + sizeof(*fd_ids_inst)); + static_assert(sizeof(*fd_ids_inst) == 16, "size is incorrect"); + Local name = FIXED_ONE_BYTE_STRING(env->isolate(), "fd_async_ids"); + Local value = + Float64Array::New(ab, 0, sizeof(*fd_ids_inst) / sizeof(double)); + target->Set(env->context(), name, value).ToChecked(); + StatWatcher::Initialize(env, target); // Create FunctionTemplate for FSReqWrap Local fst = FunctionTemplate::New(env->isolate(), NewFSReqWrap); fst->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(fst, "getAsyncId", AsyncWrap::GetAsyncId); fst->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "FSReqWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "FSReqWrap"), fst->GetFunction()); diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index f757cd6797058d..3bcc5139f73cd8 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -454,6 +454,8 @@ class Parser : public AsyncWrap { ASSIGN_OR_RETURN_UNWRAP(&parser, args.Holder()); // Should always be called from the same context. CHECK_EQ(env, parser->env()); + // The parser is being reused. Reset the uid and call init() callbacks. + parser->Reset(); parser->Init(type); } @@ -763,6 +765,7 @@ void InitHttpParser(Local target, #undef V target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "methods"), methods); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "close", Parser::Close); env->SetProtoMethod(t, "execute", Parser::Execute); env->SetProtoMethod(t, "finish", Parser::Finish); diff --git a/src/node_stat_watcher.cc b/src/node_stat_watcher.cc index cff92e34045a1e..b79313c0e60e2c 100644 --- a/src/node_stat_watcher.cc +++ b/src/node_stat_watcher.cc @@ -28,6 +28,7 @@ void StatWatcher::Initialize(Environment* env, Local target) { t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "StatWatcher")); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "start", StatWatcher::Start); env->SetProtoMethod(t, "stop", StatWatcher::Stop); diff --git a/src/node_zlib.cc b/src/node_zlib.cc index 0b85e6a1d60921..678200494ece06 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -52,7 +52,7 @@ void InitZlib(v8::Local target); class ZCtx : public AsyncWrap { public: ZCtx(Environment* env, Local wrap, node_zlib_mode mode) - : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_ZLIB), + : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_ZCTX), dictionary_(nullptr), dictionary_len_(0), err_(0), @@ -658,6 +658,7 @@ void InitZlib(Local target, z->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(z, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(z, "write", ZCtx::Write); env->SetProtoMethod(z, "writeSync", ZCtx::Write); env->SetProtoMethod(z, "init", ZCtx::Init); diff --git a/src/pipe_wrap.cc b/src/pipe_wrap.cc index 5f47dadddb4d96..9eb1aa03b2d888 100644 --- a/src/pipe_wrap.cc +++ b/src/pipe_wrap.cc @@ -17,7 +17,6 @@ namespace node { using v8::Context; using v8::EscapableHandleScope; -using v8::External; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -26,15 +25,16 @@ using v8::Local; using v8::Object; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; Local PipeWrap::Instantiate(Environment* env, AsyncWrap* parent) { EscapableHandleScope handle_scope(env->isolate()); + AsyncHooks::InitScope init_scope(env, parent->get_id()); CHECK_EQ(false, env->pipe_constructor_template().IsEmpty()); Local constructor = env->pipe_constructor_template()->GetFunction(); CHECK_EQ(false, constructor.IsEmpty()); - Local ptr = External::New(env->isolate(), parent); Local instance = - constructor->NewInstance(env->context(), 1, &ptr).ToLocalChecked(); + constructor->NewInstance(env->context()).ToLocalChecked(); return handle_scope.Escape(instance); } @@ -48,6 +48,8 @@ void PipeWrap::Initialize(Local target, t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Pipe")); t->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "close", HandleWrap::Close); env->SetProtoMethod(t, "unref", HandleWrap::Unref); env->SetProtoMethod(t, "ref", HandleWrap::Ref); @@ -77,6 +79,7 @@ void PipeWrap::Initialize(Local target, }; auto cwt = FunctionTemplate::New(env->isolate(), constructor); cwt->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(cwt, "getAsyncId", AsyncWrap::GetAsyncId); cwt->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PipeConnectWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "PipeConnectWrap"), cwt->GetFunction()); @@ -89,23 +92,16 @@ void PipeWrap::New(const FunctionCallbackInfo& args) { // normal function. CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - if (args[0]->IsExternal()) { - void* ptr = args[0].As()->Value(); - new PipeWrap(env, args.This(), false, static_cast(ptr)); - } else { - new PipeWrap(env, args.This(), args[0]->IsTrue(), nullptr); - } + new PipeWrap(env, args.This(), args[0]->IsTrue()); } PipeWrap::PipeWrap(Environment* env, Local object, - bool ipc, - AsyncWrap* parent) + bool ipc) : ConnectionWrap(env, object, - AsyncWrap::PROVIDER_PIPEWRAP, - parent) { + AsyncWrap::PROVIDER_PIPEWRAP) { int r = uv_pipe_init(env->event_loop(), &handle_, ipc); CHECK_EQ(r, 0); // How do we proxy this error up to javascript? // Suggestion: uv_pipe_init() returns void. diff --git a/src/pipe_wrap.h b/src/pipe_wrap.h index 9dcfa91bac8579..17a9dc5403c7ad 100644 --- a/src/pipe_wrap.h +++ b/src/pipe_wrap.h @@ -21,8 +21,7 @@ class PipeWrap : public ConnectionWrap { private: PipeWrap(Environment* env, v8::Local object, - bool ipc, - AsyncWrap* parent); + bool ipc); static void New(const v8::FunctionCallbackInfo& args); static void Bind(const v8::FunctionCallbackInfo& args); diff --git a/src/process_wrap.cc b/src/process_wrap.cc index 8c8e4704be4f82..3bc0bb7b21906a 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -32,6 +32,8 @@ class ProcessWrap : public HandleWrap { constructor->InstanceTemplate()->SetInternalFieldCount(1); constructor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Process")); + env->SetProtoMethod(constructor, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(constructor, "close", HandleWrap::Close); env->SetProtoMethod(constructor, "spawn", Spawn); diff --git a/src/req-wrap-inl.h b/src/req-wrap-inl.h index 84af22023dc3b9..e21fb1bdad9363 100644 --- a/src/req-wrap-inl.h +++ b/src/req-wrap-inl.h @@ -30,7 +30,6 @@ template ReqWrap::~ReqWrap() { CHECK_EQ(req_.data, this); // Assert that someone has called Dispatched(). CHECK_EQ(false, persistent().IsEmpty()); - ClearWrap(object()); persistent().Reset(); } diff --git a/src/signal_wrap.cc b/src/signal_wrap.cc index 582d1a9ecfdc02..7792c11aec60ec 100644 --- a/src/signal_wrap.cc +++ b/src/signal_wrap.cc @@ -28,6 +28,7 @@ class SignalWrap : public HandleWrap { constructor->InstanceTemplate()->SetInternalFieldCount(1); constructor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Signal")); + env->SetProtoMethod(constructor, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(constructor, "close", HandleWrap::Close); env->SetProtoMethod(constructor, "ref", HandleWrap::Ref); env->SetProtoMethod(constructor, "unref", HandleWrap::Unref); diff --git a/src/stream_base-inl.h b/src/stream_base-inl.h index da636909b695f3..8b5b15420703ef 100644 --- a/src/stream_base-inl.h +++ b/src/stream_base-inl.h @@ -23,6 +23,8 @@ using v8::PropertyCallbackInfo; using v8::String; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; + template void StreamBase::AddMethods(Environment* env, Local t, @@ -134,6 +136,7 @@ void StreamBase::JSMethod(const FunctionCallbackInfo& args) { if (!wrap->IsAlive()) return args.GetReturnValue().Set(UV_EINVAL); + AsyncHooks::InitScope init_scope(handle->env(), handle->get_id()); args.GetReturnValue().Set((wrap->*Method)(args)); } diff --git a/src/stream_base.cc b/src/stream_base.cc index 3ed622d7ef35a2..88cd6ebf997cfc 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -53,6 +53,9 @@ int StreamBase::Shutdown(const FunctionCallbackInfo& args) { CHECK(args[0]->IsObject()); Local req_wrap_obj = args[0].As(); + AsyncWrap* wrap = GetAsyncWrap(); + if (wrap != nullptr) + env->set_init_trigger_id(wrap->get_id()); ShutdownWrap* req_wrap = new ShutdownWrap(env, req_wrap_obj, this, @@ -129,6 +132,11 @@ int StreamBase::Writev(const FunctionCallbackInfo& args) { if (storage_size > INT_MAX) return UV_ENOBUFS; + AsyncWrap* wrap = GetAsyncWrap(); + // NOTE: All tests show that GetAsyncWrap() never returns nullptr here. If it + // can then replace the CHECK_NE() with if (wrap != nullptr). + CHECK_NE(wrap, nullptr); + env->set_init_trigger_id(wrap->get_id()); WriteWrap* req_wrap = WriteWrap::New(env, req_wrap_obj, this, @@ -192,6 +200,7 @@ int StreamBase::WriteBuffer(const FunctionCallbackInfo& args) { CHECK(Buffer::HasInstance(args[1])); Environment* env = Environment::GetCurrent(args); + AsyncWrap* wrap = GetAsyncWrap(); Local req_wrap_obj = args[0].As(); const char* data = Buffer::Data(args[1]); size_t length = Buffer::Length(args[1]); @@ -211,6 +220,8 @@ int StreamBase::WriteBuffer(const FunctionCallbackInfo& args) { goto done; CHECK_EQ(count, 1); + if (wrap != nullptr) + env->set_init_trigger_id(wrap->get_id()); // Allocate, or write rest req_wrap = WriteWrap::New(env, req_wrap_obj, this, AfterWrite); @@ -239,6 +250,7 @@ int StreamBase::WriteString(const FunctionCallbackInfo& args) { CHECK(args[0]->IsObject()); CHECK(args[1]->IsString()); + AsyncWrap* wrap = GetAsyncWrap(); Local req_wrap_obj = args[0].As(); Local string = args[1].As(); Local send_handle_obj; @@ -292,6 +304,8 @@ int StreamBase::WriteString(const FunctionCallbackInfo& args) { CHECK_EQ(count, 1); } + if (wrap != nullptr) + env->set_init_trigger_id(wrap->get_id()); req_wrap = WriteWrap::New(env, req_wrap_obj, this, AfterWrite, storage_size); data = req_wrap->Extra(); diff --git a/src/stream_base.h b/src/stream_base.h index faddee88c82786..e38e6c34078dd8 100644 --- a/src/stream_base.h +++ b/src/stream_base.h @@ -53,6 +53,10 @@ class ShutdownWrap : public ReqWrap, Wrap(req_wrap_obj, this); } + ~ShutdownWrap() { + ClearWrap(object()); + } + static void NewShutdownWrap(const v8::FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); } @@ -106,6 +110,10 @@ class WriteWrap: public ReqWrap, Wrap(obj, this); } + ~WriteWrap() { + ClearWrap(object()); + } + void* operator new(size_t size) = delete; void* operator new(size_t size, char* storage) { return storage; } @@ -146,10 +154,14 @@ class StreamResource { const uv_buf_t* buf, uv_handle_type pending, void* ctx); + typedef void (*DestructCb)(void* ctx); StreamResource() : bytes_read_(0) { } - virtual ~StreamResource() = default; + virtual ~StreamResource() { + if (!destruct_cb_.is_empty()) + destruct_cb_.fn(destruct_cb_.ctx); + } virtual int DoShutdown(ShutdownWrap* req_wrap) = 0; virtual int DoTryWrite(uv_buf_t** bufs, size_t* count); @@ -186,15 +198,18 @@ class StreamResource { inline void set_alloc_cb(Callback c) { alloc_cb_ = c; } inline void set_read_cb(Callback c) { read_cb_ = c; } + inline void set_destruct_cb(Callback c) { destruct_cb_ = c; } inline Callback after_write_cb() { return after_write_cb_; } inline Callback alloc_cb() { return alloc_cb_; } inline Callback read_cb() { return read_cb_; } + inline Callback destruct_cb() { return destruct_cb_; } private: Callback after_write_cb_; Callback alloc_cb_; Callback read_cb_; + Callback destruct_cb_; uint64_t bytes_read_; friend class StreamBase; diff --git a/src/stream_wrap.cc b/src/stream_wrap.cc index ac656505503b22..73ff6b0e5d7a70 100644 --- a/src/stream_wrap.cc +++ b/src/stream_wrap.cc @@ -42,6 +42,7 @@ void StreamWrap::Initialize(Local target, FunctionTemplate::New(env->isolate(), ShutdownWrap::NewShutdownWrap); sw->InstanceTemplate()->SetInternalFieldCount(1); sw->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "ShutdownWrap")); + env->SetProtoMethod(sw, "getAsyncId", AsyncWrap::GetAsyncId); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "ShutdownWrap"), sw->GetFunction()); @@ -49,6 +50,7 @@ void StreamWrap::Initialize(Local target, FunctionTemplate::New(env->isolate(), WriteWrap::NewWriteWrap); ww->InstanceTemplate()->SetInternalFieldCount(1); ww->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "WriteWrap")); + env->SetProtoMethod(ww, "getAsyncId", AsyncWrap::GetAsyncId); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "WriteWrap"), ww->GetFunction()); env->set_write_wrap_constructor_function(ww->GetFunction()); @@ -58,13 +60,11 @@ void StreamWrap::Initialize(Local target, StreamWrap::StreamWrap(Environment* env, Local object, uv_stream_t* stream, - AsyncWrap::ProviderType provider, - AsyncWrap* parent) + AsyncWrap::ProviderType provider) : HandleWrap(env, object, reinterpret_cast(stream), - provider, - parent), + provider), StreamBase(env), stream_(stream) { set_after_write_cb({ OnAfterWriteImpl, this }); diff --git a/src/stream_wrap.h b/src/stream_wrap.h index e930670202d2d8..543a269add93a3 100644 --- a/src/stream_wrap.h +++ b/src/stream_wrap.h @@ -60,8 +60,7 @@ class StreamWrap : public HandleWrap, public StreamBase { StreamWrap(Environment* env, v8::Local object, uv_stream_t* stream, - AsyncWrap::ProviderType provider, - AsyncWrap* parent = nullptr); + AsyncWrap::ProviderType provider); ~StreamWrap() { } diff --git a/src/tcp_wrap.cc b/src/tcp_wrap.cc index b2617a5695719e..e2dee8abfec010 100644 --- a/src/tcp_wrap.cc +++ b/src/tcp_wrap.cc @@ -19,7 +19,6 @@ namespace node { using v8::Boolean; using v8::Context; using v8::EscapableHandleScope; -using v8::External; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -30,15 +29,17 @@ using v8::Object; using v8::String; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; + Local TCPWrap::Instantiate(Environment* env, AsyncWrap* parent) { EscapableHandleScope handle_scope(env->isolate()); + AsyncHooks::InitScope init_scope(env, parent->get_id()); CHECK_EQ(env->tcp_constructor_template().IsEmpty(), false); Local constructor = env->tcp_constructor_template()->GetFunction(); CHECK_EQ(constructor.IsEmpty(), false); - Local ptr = External::New(env->isolate(), parent); Local instance = - constructor->NewInstance(env->context(), 1, &ptr).ToLocalChecked(); + constructor->NewInstance(env->context()).ToLocalChecked(); return handle_scope.Escape(instance); } @@ -63,6 +64,8 @@ void TCPWrap::Initialize(Local target, "onconnection"), Null(env->isolate())); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "asyncReset", AsyncWrap::AsyncReset); env->SetProtoMethod(t, "close", HandleWrap::Close); @@ -98,6 +101,7 @@ void TCPWrap::Initialize(Local target, }; auto cwt = FunctionTemplate::New(env->isolate(), constructor); cwt->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(cwt, "getAsyncId", AsyncWrap::GetAsyncId); cwt->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TCPConnectWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "TCPConnectWrap"), cwt->GetFunction()); @@ -110,24 +114,14 @@ void TCPWrap::New(const FunctionCallbackInfo& args) { // normal function. CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - TCPWrap* wrap; - if (args.Length() == 0) { - wrap = new TCPWrap(env, args.This(), nullptr); - } else if (args[0]->IsExternal()) { - void* ptr = args[0].As()->Value(); - wrap = new TCPWrap(env, args.This(), static_cast(ptr)); - } else { - UNREACHABLE(); - } - CHECK(wrap); + new TCPWrap(env, args.This()); } -TCPWrap::TCPWrap(Environment* env, Local object, AsyncWrap* parent) +TCPWrap::TCPWrap(Environment* env, Local object) : ConnectionWrap(env, object, - AsyncWrap::PROVIDER_TCPWRAP, - parent) { + AsyncWrap::PROVIDER_TCPWRAP) { int r = uv_tcp_init(env->event_loop(), &handle_); CHECK_EQ(r, 0); // How do we proxy this error up to javascript? // Suggestion: uv_tcp_init() returns void. @@ -255,6 +249,7 @@ void TCPWrap::Connect(const FunctionCallbackInfo& args) { int err = uv_ip4_addr(*ip_address, port, &addr); if (err == 0) { + env->set_init_trigger_id(wrap->get_id()); ConnectWrap* req_wrap = new ConnectWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_TCPCONNECTWRAP); err = uv_tcp_connect(req_wrap->req(), @@ -290,6 +285,7 @@ void TCPWrap::Connect6(const FunctionCallbackInfo& args) { int err = uv_ip6_addr(*ip_address, port, &addr); if (err == 0) { + env->set_init_trigger_id(wrap->get_id()); ConnectWrap* req_wrap = new ConnectWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_TCPCONNECTWRAP); err = uv_tcp_connect(req_wrap->req(), diff --git a/src/tcp_wrap.h b/src/tcp_wrap.h index 2b9e288dced6aa..750e92d5f438d3 100644 --- a/src/tcp_wrap.h +++ b/src/tcp_wrap.h @@ -25,7 +25,7 @@ class TCPWrap : public ConnectionWrap { int (*F)(const typename T::HandleType*, sockaddr*, int*)> friend void GetSockOrPeerName(const v8::FunctionCallbackInfo&); - TCPWrap(Environment* env, v8::Local object, AsyncWrap* parent); + TCPWrap(Environment* env, v8::Local object); ~TCPWrap(); static void New(const v8::FunctionCallbackInfo& args); diff --git a/src/timer_wrap.cc b/src/timer_wrap.cc index 843fde4673b071..497d156cd4754d 100644 --- a/src/timer_wrap.cc +++ b/src/timer_wrap.cc @@ -35,6 +35,8 @@ class TimerWrap : public HandleWrap { env->SetTemplateMethod(constructor, "now", Now); + env->SetProtoMethod(constructor, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(constructor, "close", HandleWrap::Close); env->SetProtoMethod(constructor, "ref", HandleWrap::Ref); env->SetProtoMethod(constructor, "unref", HandleWrap::Unref); diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc index d56128fec6c5ce..06c6db8128f3ba 100644 --- a/src/tls_wrap.cc +++ b/src/tls_wrap.cc @@ -64,6 +64,7 @@ TLSWrap::TLSWrap(Environment* env, stream_->set_after_write_cb({ OnAfterWriteImpl, this }); stream_->set_alloc_cb({ OnAllocImpl, this }); stream_->set_read_cb({ OnReadImpl, this }); + stream_->set_destruct_cb({ OnDestructImpl, this }); set_alloc_cb({ OnAllocSelf, this }); set_read_cb({ OnReadSelf, this }); @@ -522,7 +523,7 @@ int TLSWrap::GetFD() { bool TLSWrap::IsAlive() { - return ssl_ != nullptr && stream_->IsAlive(); + return ssl_ != nullptr && stream_ != nullptr && stream_->IsAlive(); } @@ -660,6 +661,12 @@ void TLSWrap::OnReadImpl(ssize_t nread, } +void TLSWrap::OnDestructImpl(void* ctx) { + TLSWrap* wrap = static_cast(ctx); + wrap->clear_stream(); +} + + void TLSWrap::OnAllocSelf(size_t suggested_size, uv_buf_t* buf, void* ctx) { buf->base = node::Malloc(suggested_size); buf->len = suggested_size; @@ -905,6 +912,8 @@ void TLSWrap::Initialize(Local target, t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TLSWrap")); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "asyncReset", AsyncWrap::AsyncReset); env->SetProtoMethod(t, "receive", Receive); env->SetProtoMethod(t, "start", Start); env->SetProtoMethod(t, "setVerifyMode", SetVerifyMode); diff --git a/src/tls_wrap.h b/src/tls_wrap.h index f390c9fe9281f7..fbf664edd530c0 100644 --- a/src/tls_wrap.h +++ b/src/tls_wrap.h @@ -54,6 +54,8 @@ class TLSWrap : public AsyncWrap, size_t self_size() const override { return sizeof(*this); } + void clear_stream() { stream_ = nullptr; } + protected: static const int kClearOutChunkSize = 16384; @@ -121,6 +123,7 @@ class TLSWrap : public AsyncWrap, const uv_buf_t* buf, uv_handle_type pending, void* ctx); + static void OnDestructImpl(void* ctx); void DoRead(ssize_t nread, const uv_buf_t* buf, uv_handle_type pending); diff --git a/src/tty_wrap.cc b/src/tty_wrap.cc index 26f061b99b34e8..ab90aa587d7cd3 100644 --- a/src/tty_wrap.cc +++ b/src/tty_wrap.cc @@ -32,6 +32,8 @@ void TTYWrap::Initialize(Local target, t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TTY")); t->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "close", HandleWrap::Close); env->SetProtoMethod(t, "unref", HandleWrap::Unref); env->SetProtoMethod(t, "hasRef", HandleWrap::HasRef); diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index d14eefd64d600a..ee3b75ab63bbf9 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -16,7 +16,6 @@ namespace node { using v8::Array; using v8::Context; using v8::EscapableHandleScope; -using v8::External; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; @@ -29,10 +28,13 @@ using v8::String; using v8::Undefined; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; + class SendWrap : public ReqWrap { public: SendWrap(Environment* env, Local req_wrap_obj, bool have_callback); + ~SendWrap(); inline bool have_callback() const; size_t msg_size; size_t self_size() const override { return sizeof(*this); } @@ -41,10 +43,15 @@ class SendWrap : public ReqWrap { }; +SendWrap::~SendWrap() { + ClearWrap(object()); +} + + SendWrap::SendWrap(Environment* env, Local req_wrap_obj, bool have_callback) - : ReqWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_UDPSENDWRAP), + : ReqWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_SENDWRAP), have_callback_(have_callback) { Wrap(req_wrap_obj, this); } @@ -60,7 +67,7 @@ static void NewSendWrap(const FunctionCallbackInfo& args) { } -UDPWrap::UDPWrap(Environment* env, Local object, AsyncWrap* parent) +UDPWrap::UDPWrap(Environment* env, Local object) : HandleWrap(env, object, reinterpret_cast(&handle_), @@ -108,6 +115,8 @@ void UDPWrap::Initialize(Local target, env->SetProtoMethod(t, "unref", HandleWrap::Unref); env->SetProtoMethod(t, "hasRef", HandleWrap::HasRef); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "UDP"), t->GetFunction()); env->set_udp_constructor_function(t->GetFunction()); @@ -115,6 +124,7 @@ void UDPWrap::Initialize(Local target, Local swt = FunctionTemplate::New(env->isolate(), NewSendWrap); swt->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(swt, "getAsyncId", AsyncWrap::GetAsyncId); swt->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "SendWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "SendWrap"), swt->GetFunction()); @@ -124,15 +134,7 @@ void UDPWrap::Initialize(Local target, void UDPWrap::New(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - if (args.Length() == 0) { - new UDPWrap(env, args.This(), nullptr); - } else if (args[0]->IsExternal()) { - new UDPWrap(env, - args.This(), - static_cast(args[0].As()->Value())); - } else { - UNREACHABLE(); - } + new UDPWrap(env, args.This()); } @@ -272,6 +274,7 @@ void UDPWrap::DoSend(const FunctionCallbackInfo& args, int family) { node::Utf8Value address(env->isolate(), args[4]); const bool have_callback = args[5]->IsTrue(); + env->set_init_trigger_id(wrap->get_id()); SendWrap* req_wrap = new SendWrap(env, req_wrap_obj, have_callback); size_t msg_size = 0; @@ -419,11 +422,12 @@ void UDPWrap::OnRecv(uv_udp_t* handle, Local UDPWrap::Instantiate(Environment* env, AsyncWrap* parent) { EscapableHandleScope scope(env->isolate()); + AsyncHooks::InitScope init_scope(env, parent->get_id()); // If this assert fires then Initialize hasn't been called yet. CHECK_EQ(env->udp_constructor_function().IsEmpty(), false); - Local ptr = External::New(env->isolate(), parent); - return scope.Escape(env->udp_constructor_function() - ->NewInstance(env->context(), 1, &ptr).ToLocalChecked()); + Local instance = env->udp_constructor_function() + ->NewInstance(env->context()).ToLocalChecked(); + return scope.Escape(instance); } diff --git a/src/udp_wrap.h b/src/udp_wrap.h index ad5f92c0f52bcb..4d5f08a2a1c2f1 100644 --- a/src/udp_wrap.h +++ b/src/udp_wrap.h @@ -47,7 +47,7 @@ class UDPWrap: public HandleWrap { int (*F)(const typename T::HandleType*, sockaddr*, int*)> friend void GetSockOrPeerName(const v8::FunctionCallbackInfo&); - UDPWrap(Environment* env, v8::Local object, AsyncWrap* parent); + UDPWrap(Environment* env, v8::Local object); static void DoBind(const v8::FunctionCallbackInfo& args, int family); diff --git a/test/common.js b/test/common.js index f111a2939b45de..5503f05cfb26b7 100644 --- a/test/common.js +++ b/test/common.js @@ -14,6 +14,54 @@ const execSync = require('child_process').execSync; const testRoot = process.env.NODE_TEST_DIR ? fs.realpathSync(process.env.NODE_TEST_DIR) : __dirname; +// If env var is set then enable async_hook hooks for all tests. +if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) { + const destroydIdsList = {}; + const destroyListList = {}; + const initHandles = {}; + const async_wrap = process.binding('async_wrap'); + + if (process.env.NODE_TEST_HANDLE_ACCESS) { + process.on('exit', () => { + // itterate through handles to make sure nothing crashes + for (const k in initHandles) + util.inspect(initHandles[k]); + }); + } + + const _addIdToDestroyList = async_wrap.addIdToDestroyList; + async_wrap.addIdToDestroyList = function addIdToDestroyList(id) { + if (!process.env.NODE_TEST_ASYNC_DESTROY) + return _addIdToDestroyList.call(this, id); + if (destroyListList[id] !== undefined) { + process._rawDebug(destroyListList[id]); + process._rawDebug(); + throw new Error(`same id added twice (${id})`); + } + destroyListList[id] = new Error().stack; + }; + + require('async_hooks').createHook({ + init(id, ty, tr, h) { + if (initHandles[id]) { + throw new Error(`init called twice for same id (${id})`); + } + initHandles[id] = h; + }, + before() { }, + after: process.env.NODE_TEST_ONLY_BEFORE_HOOK ? undefined : () => {}, + destroy(id) { + if (!process.env.NODE_TEST_ASYNC_DESTROY) return; + if (destroydIdsList[id] !== undefined) { + process._rawDebug(destroydIdsList[id]); + process._rawDebug(); + throw new Error(`destroy called for same id (${id})`); + } + destroydIdsList[id] = new Error().stack; + }, + }).enable(); +} + exports.fixturesDir = path.join(__dirname, 'fixtures'); exports.tmpDirName = 'tmp'; // PORT should match the definition in test/testpy/__init__.py. diff --git a/test/parallel/test-async-wrap-after-uncaughtexception.js b/test/parallel/test-async-wrap-after-uncaughtexception.js new file mode 100644 index 00000000000000..082d52fec2ea80 --- /dev/null +++ b/test/parallel/test-async-wrap-after-uncaughtexception.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const thrown_ids = {}; + +process.on('exit', () => { + process.removeAllListeners('uncaughtException'); + assert.strictEqual(Object.keys(thrown_ids).length, 0, 'ids remain'); +}); + +process.on('uncaughtException', common.mustCall((err) => { + assert.strictEqual(err.message, thrown_ids[async_hooks.currentId()]); +}, 5)); + +async_hooks.createHook({ + after(id) { + delete thrown_ids[id]; + }, +}).enable(); + +const si = setImmediate(() => { + throw new Error('setImmediate'); +}); +let async_id_symbol = null; +for (const i of Object.getOwnPropertySymbols(si)) { + if (i.toString() === 'Symbol(asyncId)') { + async_id_symbol = i; + break; + } +} +assert.ok(async_id_symbol !== null); +thrown_ids[si[async_id_symbol]] = 'setImmediate'; + +const st = setTimeout(() => { + throw new Error('setTimeout'); +}, 10); +thrown_ids[st[async_id_symbol]] = 'setTimeout'; + +const sin = setInterval(function() { + clearInterval(this); + throw new Error('setInterval'); +}, 10); +thrown_ids[sin[async_id_symbol]] = 'setInterval'; + +const rbId = require('crypto').randomBytes(1, () => { + throw new Error('RANDOMBYTESREQUEST'); +}).getAsyncId(); +thrown_ids[rbId] = 'RANDOMBYTESREQUEST'; + +const tcpId = require('net').createServer(function(c) { + c.end(); + setImmediate(() => this.close()); + throw new Error('TCPWRAP'); +}).listen(common.PORT)._handle.getAsyncId(); +thrown_ids[tcpId] = 'TCPWRAP'; +require('net').connect(common.PORT, () => {}).resume(); diff --git a/test/parallel/test-async-wrap-check-providers.js b/test/parallel/test-async-wrap-check-providers.js deleted file mode 100644 index 354534a6b30a5a..00000000000000 --- a/test/parallel/test-async-wrap-check-providers.js +++ /dev/null @@ -1,124 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); - return; -} - -const assert = require('assert'); -const crypto = require('crypto'); -const dgram = require('dgram'); -const dns = require('dns'); -const fs = require('fs'); -const net = require('net'); -const tls = require('tls'); -const zlib = require('zlib'); -const ChildProcess = require('child_process').ChildProcess; -const StreamWrap = require('_stream_wrap').StreamWrap; -const HTTPParser = process.binding('http_parser').HTTPParser; -const async_wrap = process.binding('async_wrap'); -const pkeys = Object.keys(async_wrap.Providers); - -let keyList = pkeys.slice(); -// Drop NONE -keyList.splice(0, 1); - -// fs-watch currently needs special configuration on AIX and we -// want to improve under https://github.com/nodejs/node/issues/5085. -// strip out fs watch related parts for now -if (common.isAix) { - for (let i = 0; i < keyList.length; i++) { - if ((keyList[i] === 'FSEVENTWRAP') || (keyList[i] === 'STATWATCHER')) { - keyList.splice(i, 1); - } - } -} - -function init(id, provider) { - keyList = keyList.filter((e) => e !== pkeys[provider]); -} - -function noop() { } - -async_wrap.setupHooks({ init }); - -async_wrap.enable(); - - -setTimeout(function() { }, 1); - -fs.stat(__filename, noop); - -if (!common.isAix) { - // fs-watch currently needs special configuration on AIX and we - // want to improve under https://github.com/nodejs/node/issues/5085. - // strip out fs watch related parts for now - fs.watchFile(__filename, noop); - fs.unwatchFile(__filename); - fs.watch(__filename).close(); -} - -dns.lookup('localhost', noop); -dns.lookupService('::', 0, noop); -dns.resolve('localhost', noop); - -new StreamWrap(new net.Socket()); - -new (process.binding('tty_wrap').TTY)(); - -crypto.randomBytes(1, noop); - -common.refreshTmpDir(); - -net.createServer(function(c) { - c.end(); - this.close(); -}).listen(common.PIPE, function() { - net.connect(common.PIPE, noop); -}); - -net.createServer(function(c) { - c.end(); - this.close(checkTLS); -}).listen(0, function() { - net.connect(this.address().port, noop); -}); - -dgram.createSocket('udp4').bind(0, function() { - this.send(Buffer.allocUnsafe(2), 0, 2, this.address().port, '::', () => { - this.close(); - }); -}); - -process.on('SIGINT', () => process.exit()); - -// Run from closed net server above. -function checkTLS() { - const options = { - key: fs.readFileSync(common.fixturesDir + '/keys/ec-key.pem'), - cert: fs.readFileSync(common.fixturesDir + '/keys/ec-cert.pem') - }; - const server = tls.createServer(options, noop) - .listen(0, function() { - const connectOpts = { rejectUnauthorized: false }; - tls.connect(this.address().port, connectOpts, function() { - this.destroy(); - server.close(); - }); - }); -} - -zlib.createGzip(); - -new ChildProcess(); - -new HTTPParser(HTTPParser.REQUEST); - -process.on('exit', function() { - if (keyList.length !== 0) { - process._rawDebug('Not all keys have been used:'); - process._rawDebug(keyList); - assert.strictEqual(keyList.length, 0); - } -}); diff --git a/test/parallel/test-async-wrap-disabled-propagate-parent.js b/test/parallel/test-async-wrap-disabled-propagate-parent.js deleted file mode 100644 index 4a6df7500956bf..00000000000000 --- a/test/parallel/test-async-wrap-disabled-propagate-parent.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -require('../common'); -const assert = require('assert'); -const net = require('net'); -const async_wrap = process.binding('async_wrap'); -const providers = Object.keys(async_wrap.Providers); - -const uidSymbol = Symbol('uid'); - -let cntr = 0; -let client; - -function init(uid, type, parentUid, parentHandle) { - this[uidSymbol] = uid; - - if (parentHandle) { - cntr++; - // Cannot assert in init callback or will abort. - process.nextTick(() => { - assert.strictEqual(providers[type], 'TCPWRAP'); - assert.strictEqual(parentUid, server._handle[uidSymbol], - 'server uid doesn\'t match parent uid'); - assert.strictEqual(parentHandle, server._handle, - 'server handle doesn\'t match parent handle'); - assert.strictEqual(this, client._handle, 'client doesn\'t match context'); - }); - } -} - -function noop() { } - -async_wrap.setupHooks({ init }); -async_wrap.enable(); - -const server = net.createServer(function(c) { - client = c; - // Allow init callback to run before closing. - setImmediate(() => { - c.end(); - this.close(); - }); -}).listen(0, function() { - net.connect(this.address().port, noop); -}); - -async_wrap.disable(); - -process.on('exit', function() { - // init should have only been called once with a parent. - assert.strictEqual(cntr, 1); -}); diff --git a/test/parallel/test-async-wrap-post-did-throw.js b/test/parallel/test-async-wrap-post-did-throw.js deleted file mode 100644 index 35dbfe1378464a..00000000000000 --- a/test/parallel/test-async-wrap-post-did-throw.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); - return; -} - -const assert = require('assert'); -const async_wrap = process.binding('async_wrap'); -let asyncThrows = 0; -let uncaughtExceptionCount = 0; - -process.on('uncaughtException', (e) => { - assert.strictEqual(e.message, 'oh noes!', 'error messages do not match'); -}); - -process.on('exit', () => { - process.removeAllListeners('uncaughtException'); - assert.strictEqual(uncaughtExceptionCount, 1); - assert.strictEqual(uncaughtExceptionCount, asyncThrows); -}); - -function init() { } -function post(id, threw) { - if (threw) - uncaughtExceptionCount++; -} - -async_wrap.setupHooks({ init, post }); -async_wrap.enable(); - -// Timers still aren't supported, so use crypto API. -// It's also important that the callback not happen in a nextTick, like many -// error events in core. -require('crypto').randomBytes(0, () => { - asyncThrows++; - throw new Error('oh noes!'); -}); diff --git a/test/parallel/test-async-wrap-propagate-parent.js b/test/parallel/test-async-wrap-propagate-parent.js deleted file mode 100644 index 0968490f5975cc..00000000000000 --- a/test/parallel/test-async-wrap-propagate-parent.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -require('../common'); -const assert = require('assert'); -const net = require('net'); -const async_wrap = process.binding('async_wrap'); -const providers = Object.keys(async_wrap.Providers); - -const uidSymbol = Symbol('uid'); - -let cntr = 0; -let client; - -function init(uid, type, parentUid, parentHandle) { - this[uidSymbol] = uid; - - if (parentHandle) { - cntr++; - // Cannot assert in init callback or will abort. - process.nextTick(() => { - assert.strictEqual(providers[type], 'TCPWRAP'); - assert.strictEqual(parentUid, server._handle[uidSymbol], - 'server uid doesn\'t match parent uid'); - assert.strictEqual(parentHandle, server._handle, - 'server handle doesn\'t match parent handle'); - assert.strictEqual(this, client._handle, 'client doesn\'t match context'); - }); - } -} - -function noop() { } - -async_wrap.setupHooks({ init }); -async_wrap.enable(); - -const server = net.createServer(function(c) { - client = c; - // Allow init callback to run before closing. - setImmediate(() => { - c.end(); - this.close(); - }); -}).listen(0, function() { - net.connect(this.address().port, noop); -}); - - -process.on('exit', function() { - // init should have only been called once with a parent. - assert.strictEqual(cntr, 1); -}); diff --git a/test/parallel/test-async-wrap-throw-from-callback.js b/test/parallel/test-async-wrap-throw-from-callback.js deleted file mode 100644 index c4a6a9bdaddd1c..00000000000000 --- a/test/parallel/test-async-wrap-throw-from-callback.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); - return; -} - -const async_wrap = process.binding('async_wrap'); -const assert = require('assert'); -const crypto = require('crypto'); -const domain = require('domain'); -const spawn = require('child_process').spawn; -const callbacks = [ 'init', 'pre', 'post', 'destroy' ]; -const toCall = process.argv[2]; -let msgCalled = 0; -let msgReceived = 0; - -function init() { - if (toCall === 'init') - throw new Error('init'); -} -function pre() { - if (toCall === 'pre') - throw new Error('pre'); -} -function post() { - if (toCall === 'post') - throw new Error('post'); -} -function destroy() { - if (toCall === 'destroy') - throw new Error('destroy'); -} - -if (typeof process.argv[2] === 'string') { - async_wrap.setupHooks({ init, pre, post, destroy }); - async_wrap.enable(); - - process.on('uncaughtException', common.fail); - - const d = domain.create(); - d.on('error', common.fail); - d.run(() => { - // Using randomBytes because timers are not yet supported. - crypto.randomBytes(0, () => { }); - }); - -} else { - - process.on('exit', (code) => { - assert.strictEqual(msgCalled, callbacks.length); - assert.strictEqual(msgCalled, msgReceived); - }); - - callbacks.forEach((item) => { - msgCalled++; - - const child = spawn(process.execPath, [__filename, item]); - let errstring = ''; - - child.stderr.on('data', (data) => { - errstring += data.toString(); - }); - - child.on('close', (code) => { - if (errstring.includes('Error: ' + item)) - msgReceived++; - - assert.strictEqual(code, 1, `${item} closed with code ${code}`); - }); - }); -} diff --git a/test/parallel/test-async-wrap-throw-no-init.js b/test/parallel/test-async-wrap-throw-no-init.js deleted file mode 100644 index 1f8cc70b36c6e7..00000000000000 --- a/test/parallel/test-async-wrap-throw-no-init.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -require('../common'); -const assert = require('assert'); -const async_wrap = process.binding('async_wrap'); - -assert.throws(function() { - async_wrap.setupHooks(null); -}, /first argument must be an object/); - -assert.throws(function() { - async_wrap.setupHooks({}); -}, /init callback must be a function/); - -assert.throws(function() { - async_wrap.enable(); -}, /init callback is not assigned to a function/); - -// Should not throw -async_wrap.setupHooks({ init: () => {} }); -async_wrap.enable(); - -assert.throws(function() { - async_wrap.setupHooks(() => {}); -}, /hooks should not be set while also enabled/); diff --git a/test/parallel/test-async-wrap-uid.js b/test/parallel/test-async-wrap-uid.js deleted file mode 100644 index f16388cfe766f0..00000000000000 --- a/test/parallel/test-async-wrap-uid.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -require('../common'); -const fs = require('fs'); -const assert = require('assert'); -const async_wrap = process.binding('async_wrap'); - -// Give the event loop time to clear out the final uv_close(). -let si_cntr = 3; -process.on('beforeExit', () => { - if (--si_cntr > 0) setImmediate(() => {}); -}); - -const storage = new Map(); -async_wrap.setupHooks({ init, pre, post, destroy }); -async_wrap.enable(); - -function init(uid) { - storage.set(uid, { - init: true, - pre: false, - post: false, - destroy: false, - }); -} - -function pre(uid) { - storage.get(uid).pre = true; -} - -function post(uid) { - storage.get(uid).post = true; -} - -function destroy(uid) { - storage.get(uid).destroy = true; -} - -fs.access(__filename, function(err) { - assert.ifError(err); -}); - -fs.access(__filename, function(err) { - assert.ifError(err); -}); - -async_wrap.disable(); - -process.once('exit', function() { - assert.strictEqual(storage.size, 2); - - for (const item of storage) { - const uid = item[0]; - const value = item[1]; - assert.strictEqual(typeof uid, 'number'); - assert.deepStrictEqual(value, { - init: true, - pre: true, - post: true, - destroy: true, - }); - } -}); diff --git a/test/parallel/test-net-end-close.js b/test/parallel/test-net-end-close.js index d9f33fd7d8d1cf..36882f073da705 100644 --- a/test/parallel/test-net-end-close.js +++ b/test/parallel/test-net-end-close.js @@ -10,7 +10,8 @@ const s = new net.Socket({ readStart: function() { process.nextTick(() => this.onread(uv.UV_EOF, null)); }, - close: (cb) => process.nextTick(cb) + close: (cb) => process.nextTick(cb), + getAsyncId: () => 0, }, writable: false }); diff --git a/test/parallel/test-stream-base-no-abort.js b/test/parallel/test-stream-base-no-abort.js deleted file mode 100644 index 8b85acea2fb7ea..00000000000000 --- a/test/parallel/test-stream-base-no-abort.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); - return; -} - -const async_wrap = process.binding('async_wrap'); -const uv = process.binding('uv'); -const assert = require('assert'); -const dgram = require('dgram'); -const fs = require('fs'); -const net = require('net'); -const tls = require('tls'); -const providers = Object.keys(async_wrap.Providers); -let flags = 0; - -// Make sure all asserts have run at least once. -process.on('exit', () => assert.strictEqual(flags, 0b111)); - -function init(id, provider) { - this._external; // Test will abort if nullptr isn't properly checked. - switch (providers[provider]) { - case 'TCPWRAP': - assert.strictEqual(this.fd, uv.UV_EINVAL); - flags |= 0b1; - break; - case 'TLSWRAP': - assert.strictEqual(this.fd, uv.UV_EINVAL); - flags |= 0b10; - break; - case 'UDPWRAP': - assert.strictEqual(this.fd, uv.UV_EBADF); - flags |= 0b100; - break; - } -} - -async_wrap.setupHooks({ init }); -async_wrap.enable(); - -const checkTLS = common.mustCall(function checkTLS() { - const options = { - key: fs.readFileSync(common.fixturesDir + '/keys/ec-key.pem'), - cert: fs.readFileSync(common.fixturesDir + '/keys/ec-cert.pem') - }; - const server = tls.createServer(options, () => {}) - .listen(0, function() { - const connectOpts = { rejectUnauthorized: false }; - tls.connect(this.address().port, connectOpts, function() { - this.destroy(); - server.close(); - }); - }); -}); - -const checkTCP = common.mustCall(function checkTCP() { - net.createServer(() => {}).listen(0, function() { - this.close(checkTLS); - }); -}); - -dgram.createSocket('udp4').close(checkTCP); diff --git a/test/parallel/test-tls-retain-handle-no-abort.js b/test/parallel/test-tls-retain-handle-no-abort.js new file mode 100644 index 00000000000000..43b3709fd5f85b --- /dev/null +++ b/test/parallel/test-tls-retain-handle-no-abort.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} +const tls = require('tls'); +const fs = require('fs'); +const util = require('util'); + +const sent = 'hello world'; +const serverOptions = { + isServer: true, + key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'), + cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem') +}; + +let ssl = null; + +process.on('exit', function() { + assert.ok(ssl !== null); + // If the internal pointer to stream_ isn't cleared properly then this + // will abort. + util.inspect(ssl); +}); + +const server = tls.createServer(serverOptions, function(s) { + s.on('data', function() { }); + s.on('end', function() { + server.close(); + s.destroy(); + }); +}).listen(0, function() { + const c = new tls.TLSSocket(); + ssl = c.ssl; + c.connect(this.address().port, function() { + c.end(sent); + }); +});