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 <anna@addaleax.net> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
parent
9545c48a5e
commit
d62566e1b1
@ -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 {
|
||||
|
@ -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})`);
|
||||
}
|
||||
process._setupPromises(unhandledRejection, handledRejection);
|
||||
|
||||
function setupPromises(scheduleMicrotasks) {
|
||||
let deprecationWarned = false;
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
function unhandledRejection(promise, reason) {
|
||||
hasBeenNotifiedProperty.set(promise, false);
|
||||
promiseToGuidProperty.set(promise, lastPromiseId++);
|
||||
addPendingUnhandledRejection(promise, reason);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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 = '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');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hadListeners;
|
||||
}
|
||||
|
||||
function addPendingUnhandledRejection(promise, reason) {
|
||||
pendingUnhandledRejections.push(promise, reason);
|
||||
scheduleMicrotasks();
|
||||
}
|
||||
|
||||
return emitPendingUnhandledRejections;
|
||||
pendingUnhandledRejections.push(promise);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 emitPromiseRejectionWarnings() {
|
||||
while (asyncHandledRejections.length > 0) {
|
||||
const { promise, warning } = asyncHandledRejections.shift();
|
||||
if (!process.emit('rejectionHandled', promise)) {
|
||||
process.emitWarning(warning);
|
||||
}
|
||||
}
|
||||
|
||||
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 hadListeners || pendingUnhandledRejections.length !== 0;
|
||||
}
|
||||
|
@ -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<v8::Context> context,
|
||||
const ContextInfo& info) {
|
||||
context->SetAlignedPointerInEmbedderData(kContextEmbedderDataIndex, this);
|
||||
|
@ -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<uint8_t, v8::Uint8Array>& 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
|
||||
};
|
||||
|
||||
|
48
src/node.cc
48
src/node.cc
@ -891,19 +891,33 @@ void SetupNextTick(const FunctionCallbackInfo<Value>& args) {
|
||||
void PromiseRejectCallback(PromiseRejectMessage message) {
|
||||
Local<Promise> promise = message.GetPromise();
|
||||
Isolate* isolate = promise->GetIsolate();
|
||||
Local<Value> value = message.GetValue();
|
||||
Local<Integer> event = Integer::New(isolate, message.GetEvent());
|
||||
v8::PromiseRejectEvent event = message.GetEvent();
|
||||
|
||||
Environment* env = Environment::GetCurrent(isolate);
|
||||
Local<Function> callback = env->promise_reject_function();
|
||||
Local<Function> callback;
|
||||
Local<Value> 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<Value> args[] = { event, promise, value };
|
||||
Local<Object> process = env->process_object();
|
||||
Local<Value> args[] = { promise, value };
|
||||
MaybeLocal<Value> 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<Value>& args) {
|
||||
@ -911,9 +925,11 @@ void SetupPromises(const FunctionCallbackInfo<Value>& args) {
|
||||
Isolate* isolate = env->isolate();
|
||||
|
||||
CHECK(args[0]->IsFunction());
|
||||
CHECK(args[1]->IsFunction());
|
||||
|
||||
isolate->SetPromiseRejectCallback(PromiseRejectCallback);
|
||||
env->set_promise_reject_function(args[0].As<Function>());
|
||||
env->set_promise_reject_unhandled_function(args[0].As<Function>());
|
||||
env->set_promise_reject_handled_function(args[1].As<Function>());
|
||||
|
||||
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<Object> 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)
|
||||
|
@ -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 *
|
||||
|
@ -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()));
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user