From a84ec52dd47d318910c5c24009cafbf6d88836fb Mon Sep 17 00:00:00 2001 From: Anatoli Papirovski Date: Wed, 17 Jan 2018 09:39:01 -0500 Subject: [PATCH] promises: refactor rejection handling Remove the unnecessary microTasksTickObject for scheduling microtasks and instead use TickInfo to keep track of whether promise rejections exist that need to be emitted. Consequently allow the microtasks to execute on average fewer times, in more predictable manner than previously. Simplify unhandled & handled rejection tracking to do more in C++ to avoid needing to expose additional info in JS. When new unhandledRejections are emitted within an unhandledRejection handler, allow the event loop to proceed first instead. This means that if the end-user code handles all promise rejections on nextTick, rejections within unhandledRejection now won't spiral into an infinite loop. PR-URL: https://github.com/nodejs/node/pull/18207 Fixes: https://github.com/nodejs/node/issues/17913 Reviewed-By: Anna Henningsen Reviewed-By: Benjamin Gruenbaum --- lib/internal/process/next_tick.js | 33 +--- lib/internal/process/promises.js | 186 ++++++++---------- src/env-inl.h | 8 + src/env.h | 7 +- src/node.cc | 48 ++--- .../unhandled_promise_trace_warnings.out | 5 +- .../test-promises-unhandled-rejections.js | 13 ++ 7 files changed, 143 insertions(+), 157 deletions(-) diff --git a/lib/internal/process/next_tick.js b/lib/internal/process/next_tick.js index 9481bebd224be6..e7fad84397c427 100644 --- a/lib/internal/process/next_tick.js +++ b/lib/internal/process/next_tick.js @@ -9,7 +9,7 @@ function setupNextTick() { const async_hooks = require('internal/async_hooks'); const promises = require('internal/process/promises'); const errors = require('internal/errors'); - const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks); + const { emitPromiseRejectionWarnings } = promises; const getDefaultTriggerAsyncId = async_hooks.getDefaultTriggerAsyncId; // Two arrays that share state between C++ and JS. const { async_hook_fields, async_id_fields } = async_wrap; @@ -30,6 +30,7 @@ function setupNextTick() { // *Must* match Environment::TickInfo::Fields in src/env.h. const kHasScheduled = 0; + const kHasPromiseRejections = 1; const nextTickQueue = { head: null, @@ -58,8 +59,6 @@ function setupNextTick() { } }; - var microtasksScheduled = false; - process.nextTick = nextTick; // Needs to be accessible from beyond this scope. process._tickCallback = _tickCallback; @@ -67,30 +66,6 @@ function setupNextTick() { // Set the nextTick() function for internal usage. exports.nextTick = internalNextTick; - const microTasksTickObject = { - callback: runMicrotasksCallback, - args: undefined, - [async_id_symbol]: 0, - [trigger_async_id_symbol]: 0 - }; - function scheduleMicrotasks() { - if (microtasksScheduled) - return; - - // For the moment all microtasks come from the void until the PromiseHook - // API is implemented. - nextTickQueue.push(microTasksTickObject); - microtasksScheduled = true; - } - - function runMicrotasksCallback() { - microtasksScheduled = false; - runMicrotasks(); - - if (nextTickQueue.head !== null || emitPendingUnhandledRejections()) - scheduleMicrotasks(); - } - function _tickCallback() { let tock; do { @@ -118,8 +93,8 @@ function setupNextTick() { emitAfter(asyncId); } runMicrotasks(); - emitPendingUnhandledRejections(); - } while (nextTickQueue.head !== null); + } while (nextTickQueue.head !== null || emitPromiseRejectionWarnings()); + tickInfo[kHasPromiseRejections] = 0; } class TickObject { diff --git a/lib/internal/process/promises.js b/lib/internal/process/promises.js index 82f15f9f9df51e..a1c7331a0d98ba 100644 --- a/lib/internal/process/promises.js +++ b/lib/internal/process/promises.js @@ -2,123 +2,109 @@ const { safeToString } = process.binding('util'); -const promiseRejectEvent = process._promiseRejectEvent; -const hasBeenNotifiedProperty = new WeakMap(); -const promiseToGuidProperty = new WeakMap(); +const maybeUnhandledPromises = new WeakMap(); const pendingUnhandledRejections = []; -let lastPromiseId = 1; +const asyncHandledRejections = []; +let lastPromiseId = 0; -exports.setup = setupPromises; +module.exports = { + emitPromiseRejectionWarnings +}; -function getAsynchronousRejectionWarningObject(uid) { - return new Error('Promise rejection was handled ' + - `asynchronously (rejection id: ${uid})`); -} - -function setupPromises(scheduleMicrotasks) { - let deprecationWarned = false; +process._setupPromises(unhandledRejection, handledRejection); - process._setupPromises(function(event, promise, reason) { - if (event === promiseRejectEvent.unhandled) - unhandledRejection(promise, reason); - else if (event === promiseRejectEvent.handled) - rejectionHandled(promise); - else - require('assert').fail(null, null, 'unexpected PromiseRejectEvent'); +function unhandledRejection(promise, reason) { + maybeUnhandledPromises.set(promise, { + reason, + uid: ++lastPromiseId, + warned: false }); + pendingUnhandledRejections.push(promise); + return true; +} - function unhandledRejection(promise, reason) { - hasBeenNotifiedProperty.set(promise, false); - promiseToGuidProperty.set(promise, lastPromiseId++); - addPendingUnhandledRejection(promise, reason); +function handledRejection(promise) { + const promiseInfo = maybeUnhandledPromises.get(promise); + if (promiseInfo !== undefined) { + maybeUnhandledPromises.delete(promise); + if (promiseInfo.warned) { + const { uid } = promiseInfo; + // Generate the warning object early to get a good stack trace. + const warning = new Error('Promise rejection was handled ' + + `asynchronously (rejection id: ${uid})`); + warning.name = 'PromiseRejectionHandledWarning'; + warning.id = uid; + asyncHandledRejections.push({ promise, warning }); + return true; + } } + return false; +} - function rejectionHandled(promise) { - const hasBeenNotified = hasBeenNotifiedProperty.get(promise); - if (hasBeenNotified !== undefined) { - hasBeenNotifiedProperty.delete(promise); - const uid = promiseToGuidProperty.get(promise); - promiseToGuidProperty.delete(promise); - if (hasBeenNotified === true) { - let warning = null; - if (!process.listenerCount('rejectionHandled')) { - // Generate the warning object early to get a good stack trace. - warning = getAsynchronousRejectionWarningObject(uid); - } - process.nextTick(function() { - if (!process.emit('rejectionHandled', promise)) { - if (warning === null) - warning = getAsynchronousRejectionWarningObject(uid); - warning.name = 'PromiseRejectionHandledWarning'; - warning.id = uid; - process.emitWarning(warning); - } - }); - } - +const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning'; +function emitWarning(uid, reason) { + try { + if (reason instanceof Error) { + process.emitWarning(reason.stack, unhandledRejectionErrName); + } else { + process.emitWarning(safeToString(reason), unhandledRejectionErrName); } + } catch (e) { + // ignored } - function emitWarning(uid, reason) { - try { - if (reason instanceof Error) { - process.emitWarning(reason.stack, 'UnhandledPromiseRejectionWarning'); - } else { - process.emitWarning( - safeToString(reason), 'UnhandledPromiseRejectionWarning' - ); - } - } catch (e) { - // ignored + const warning = new Error( + 'Unhandled promise rejection. This error originated either by ' + + 'throwing inside of an async function without a catch block, ' + + 'or by rejecting a promise which was not handled with .catch(). ' + + `(rejection id: ${uid})` + ); + warning.name = unhandledRejectionErrName; + try { + if (reason instanceof Error) { + warning.stack = reason.stack; } + } catch (err) { + // ignored + } + process.emitWarning(warning); + emitDeprecationWarning(); +} - const warning = new Error( - 'Unhandled promise rejection. This error originated either by ' + - 'throwing inside of an async function without a catch block, ' + - 'or by rejecting a promise which was not handled with .catch(). ' + - `(rejection id: ${uid})` - ); - warning.name = 'UnhandledPromiseRejectionWarning'; - try { - if (reason instanceof Error) { - warning.stack = reason.stack; - } - } catch (err) { - // ignored - } - process.emitWarning(warning); - if (!deprecationWarned) { - deprecationWarned = true; - process.emitWarning( - 'Unhandled promise rejections are deprecated. In the future, ' + - 'promise rejections that are not handled will terminate the ' + - 'Node.js process with a non-zero exit code.', - 'DeprecationWarning', 'DEP0018'); - } +let deprecationWarned = false; +function emitDeprecationWarning() { + if (!deprecationWarned) { + deprecationWarned = true; + process.emitWarning( + 'Unhandled promise rejections are deprecated. In the future, ' + + 'promise rejections that are not handled will terminate the ' + + 'Node.js process with a non-zero exit code.', + 'DeprecationWarning', 'DEP0018'); } +} - function emitPendingUnhandledRejections() { - let hadListeners = false; - while (pendingUnhandledRejections.length > 0) { - const promise = pendingUnhandledRejections.shift(); - const reason = pendingUnhandledRejections.shift(); - if (hasBeenNotifiedProperty.get(promise) === false) { - hasBeenNotifiedProperty.set(promise, true); - const uid = promiseToGuidProperty.get(promise); - if (!process.emit('unhandledRejection', reason, promise)) { - emitWarning(uid, reason); - } else { - hadListeners = true; - } - } +function emitPromiseRejectionWarnings() { + while (asyncHandledRejections.length > 0) { + const { promise, warning } = asyncHandledRejections.shift(); + if (!process.emit('rejectionHandled', promise)) { + process.emitWarning(warning); } - return hadListeners; } - function addPendingUnhandledRejection(promise, reason) { - pendingUnhandledRejections.push(promise, reason); - scheduleMicrotasks(); + let hadListeners = false; + let len = pendingUnhandledRejections.length; + while (len--) { + const promise = pendingUnhandledRejections.shift(); + const promiseInfo = maybeUnhandledPromises.get(promise); + if (promiseInfo !== undefined) { + promiseInfo.warned = true; + const { reason, uid } = promiseInfo; + if (!process.emit('unhandledRejection', reason, promise)) { + emitWarning(uid, reason); + } else { + hadListeners = true; + } + } } - - return emitPendingUnhandledRejections; + return hadListeners || pendingUnhandledRejections.length !== 0; } diff --git a/src/env-inl.h b/src/env-inl.h index 4fec606219a036..910bba69f7569f 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -264,6 +264,14 @@ inline bool Environment::TickInfo::has_scheduled() const { return fields_[kHasScheduled] == 1; } +inline bool Environment::TickInfo::has_promise_rejections() const { + return fields_[kHasPromiseRejections] == 1; +} + +inline void Environment::TickInfo::promise_rejections_toggle_on() { + fields_[kHasPromiseRejections] = 1; +} + inline void Environment::AssignToContext(v8::Local context, const ContextInfo& info) { context->SetAlignedPointerInEmbedderData(kContextEmbedderDataIndex, this); diff --git a/src/env.h b/src/env.h index a9f2af0a94748e..813b2f5a62448a 100644 --- a/src/env.h +++ b/src/env.h @@ -293,7 +293,8 @@ class ModuleWrap; V(performance_entry_callback, v8::Function) \ V(performance_entry_template, v8::Function) \ V(process_object, v8::Object) \ - V(promise_reject_function, v8::Function) \ + V(promise_reject_handled_function, v8::Function) \ + V(promise_reject_unhandled_function, v8::Function) \ V(promise_wrap_template, v8::ObjectTemplate) \ V(push_values_to_array_function, v8::Function) \ V(randombytes_constructor_template, v8::ObjectTemplate) \ @@ -482,6 +483,9 @@ class Environment { public: inline AliasedBuffer& fields(); inline bool has_scheduled() const; + inline bool has_promise_rejections() const; + + inline void promise_rejections_toggle_on(); private: friend class Environment; // So we can call the constructor. @@ -489,6 +493,7 @@ class Environment { enum Fields { kHasScheduled, + kHasPromiseRejections, kFieldsCount }; diff --git a/src/node.cc b/src/node.cc index d01276800a911f..0ceddaa64ea6cb 100644 --- a/src/node.cc +++ b/src/node.cc @@ -891,19 +891,33 @@ void SetupNextTick(const FunctionCallbackInfo& args) { void PromiseRejectCallback(PromiseRejectMessage message) { Local promise = message.GetPromise(); Isolate* isolate = promise->GetIsolate(); - Local value = message.GetValue(); - Local event = Integer::New(isolate, message.GetEvent()); + v8::PromiseRejectEvent event = message.GetEvent(); Environment* env = Environment::GetCurrent(isolate); - Local callback = env->promise_reject_function(); + Local callback; + Local value; - if (value.IsEmpty()) + if (event == v8::kPromiseRejectWithNoHandler) { + callback = env->promise_reject_unhandled_function(); + value = message.GetValue(); + + if (value.IsEmpty()) + value = Undefined(isolate); + } else if (event == v8::kPromiseHandlerAddedAfterReject) { + callback = env->promise_reject_handled_function(); value = Undefined(isolate); + } else { + UNREACHABLE(); + } - Local args[] = { event, promise, value }; - Local process = env->process_object(); + Local args[] = { promise, value }; + MaybeLocal ret = callback->Call(env->context(), + Undefined(isolate), + arraysize(args), + args); - callback->Call(process, arraysize(args), args); + if (!ret.IsEmpty() && ret.ToLocalChecked()->IsTrue()) + env->tick_info()->promise_rejections_toggle_on(); } void SetupPromises(const FunctionCallbackInfo& args) { @@ -911,9 +925,11 @@ void SetupPromises(const FunctionCallbackInfo& args) { Isolate* isolate = env->isolate(); CHECK(args[0]->IsFunction()); + CHECK(args[1]->IsFunction()); isolate->SetPromiseRejectCallback(PromiseRejectCallback); - env->set_promise_reject_function(args[0].As()); + env->set_promise_reject_unhandled_function(args[0].As()); + env->set_promise_reject_handled_function(args[1].As()); env->process_object()->Delete( env->context(), @@ -1022,7 +1038,7 @@ void InternalCallbackScope::Close() { CHECK_EQ(env_->trigger_async_id(), 0); } - if (!tick_info->has_scheduled()) { + if (!tick_info->has_scheduled() && !tick_info->has_promise_rejections()) { return; } @@ -3038,20 +3054,6 @@ void SetupProcessObject(Environment* env, "napi", FIXED_ONE_BYTE_STRING(env->isolate(), node_napi_version)); - // process._promiseRejectEvent - Local promiseRejectEvent = Object::New(env->isolate()); - READONLY_DONT_ENUM_PROPERTY(process, - "_promiseRejectEvent", - promiseRejectEvent); - READONLY_PROPERTY(promiseRejectEvent, - "unhandled", - Integer::New(env->isolate(), - v8::kPromiseRejectWithNoHandler)); - READONLY_PROPERTY(promiseRejectEvent, - "handled", - Integer::New(env->isolate(), - v8::kPromiseHandlerAddedAfterReject)); - #if HAVE_OPENSSL // Stupid code to slice out the version string. { // NOLINT(whitespace/braces) diff --git a/test/message/unhandled_promise_trace_warnings.out b/test/message/unhandled_promise_trace_warnings.out index c9c7a5c8700b26..069e6cc678c0da 100644 --- a/test/message/unhandled_promise_trace_warnings.out +++ b/test/message/unhandled_promise_trace_warnings.out @@ -14,7 +14,6 @@ at * at * at * - at * (node:*) Error: This was rejected at * (*test*message*unhandled_promise_trace_warnings.js:*) at * @@ -34,9 +33,7 @@ at * at * (node:*) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1) - at getAsynchronousRejectionWarningObject (internal/process/promises.js:*) - at rejectionHandled (internal/process/promises.js:*) - at * + at handledRejection (internal/process/promises.js:*) at Promise.catch * at Immediate.setImmediate (*test*message*unhandled_promise_trace_warnings.js:*) at * diff --git a/test/parallel/test-promises-unhandled-rejections.js b/test/parallel/test-promises-unhandled-rejections.js index ed5d485ac72e96..1ade061994c3bb 100644 --- a/test/parallel/test-promises-unhandled-rejections.js +++ b/test/parallel/test-promises-unhandled-rejections.js @@ -684,3 +684,16 @@ asyncTest('Throwing an error inside a rejectionHandled handler goes to' + } }, 1); }); + +asyncTest('Rejected promise inside unhandledRejection allows nextTick loop' + + ' to proceed first', function(done) { + clean(); + Promise.reject(0); + let didCall = false; + process.on('unhandledRejection', () => { + assert(!didCall); + didCall = true; + const promise = Promise.reject(0); + process.nextTick(() => promise.catch(() => done())); + }); +});