Skip to content

Commit

Permalink
process: make tick callback and promise rejection callback more robust
Browse files Browse the repository at this point in the history
- Rename `internalTickCallback` to `processTicksAndRejections`, make
  sure it does not get called if it's not set in C++.
- Rename `emitPromiseRejectionWarnings` to `processPromiseRejections`
  since it also emit events that are not warnings.
- Sets `SetPromiseRejectCallback` in the `Environment` constructor
  to make sure it only gets called once per-isolate, and make
  sure it does not get called if it's not set in C++.
- Wrap promise rejection callback initialization into
  `listenForRejections()`.
- Add comments.

PR-URL: nodejs#25200
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
joyeecheung authored and refack committed Jan 10, 2019
1 parent 46acbc8 commit f322d28
Show file tree
Hide file tree
Showing 9 changed files with 50 additions and 32 deletions.
20 changes: 9 additions & 11 deletions lib/internal/process/next_tick.js
Expand Up @@ -6,15 +6,14 @@ const {
tickInfo,
// Used to run V8's micro task queue.
runMicrotasks,
setTickCallback,
initializePromiseRejectCallback
setTickCallback
} = internalBinding('task_queue');

const {
setHasRejectionToWarn,
hasRejectionToWarn,
promiseRejectHandler,
emitPromiseRejectionWarnings
listenForRejections,
processPromiseRejections
} = require('internal/process/promises');

const {
Expand Down Expand Up @@ -49,10 +48,10 @@ function runNextTicks() {
if (!hasTickScheduled() && !hasRejectionToWarn())
return;

internalTickCallback();
processTicksAndRejections();
}

function internalTickCallback() {
function processTicksAndRejections() {
let tock;
do {
while (tock = queue.shift()) {
Expand Down Expand Up @@ -80,7 +79,7 @@ function internalTickCallback() {
}
setHasTickScheduled(false);
runMicrotasks();
} while (!queue.isEmpty() || emitPromiseRejectionWarnings());
} while (!queue.isEmpty() || processPromiseRejections());
setHasRejectionToWarn(false);
}

Expand Down Expand Up @@ -134,11 +133,10 @@ function nextTick(callback) {
// TODO(joyeecheung): make this a factory class so that node.js can
// control the side effects caused by the initializers.
exports.setup = function() {
// Initializes the per-isolate promise rejection callback which
// will call the handler being passed into this function.
initializePromiseRejectCallback(promiseRejectHandler);
// Sets the per-isolate promise rejection callback
listenForRejections();
// Sets the callback to be run in every tick.
setTickCallback(internalTickCallback);
setTickCallback(processTicksAndRejections);
return {
nextTick,
runNextTicks
Expand Down
15 changes: 11 additions & 4 deletions lib/internal/process/promises.js
Expand Up @@ -8,7 +8,8 @@ const {
kPromiseHandlerAddedAfterReject,
kPromiseResolveAfterResolved,
kPromiseRejectAfterResolved
}
},
setPromiseRejectCallback
} = internalBinding('task_queue');

// *Must* match Environment::TickInfo::Fields in src/env.h.
Expand Down Expand Up @@ -117,7 +118,9 @@ function emitDeprecationWarning() {
}
}

function emitPromiseRejectionWarnings() {
// If this method returns true, at least one more tick need to be
// scheduled to process any potential pending rejections
function processPromiseRejections() {
while (asyncHandledRejections.length > 0) {
const { promise, warning } = asyncHandledRejections.shift();
if (!process.emit('rejectionHandled', promise)) {
Expand All @@ -142,9 +145,13 @@ function emitPromiseRejectionWarnings() {
return maybeScheduledTicks || pendingUnhandledRejections.length !== 0;
}

function listenForRejections() {
setPromiseRejectCallback(promiseRejectHandler);
}

module.exports = {
hasRejectionToWarn,
setHasRejectionToWarn,
promiseRejectHandler,
emitPromiseRejectionWarnings
listenForRejections,
processPromiseRejections
};
10 changes: 8 additions & 2 deletions src/callback_scope.cc
Expand Up @@ -5,6 +5,7 @@

namespace node {

using v8::Function;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
Expand Down Expand Up @@ -114,8 +115,13 @@ void InternalCallbackScope::Close() {

if (!env_->can_call_into_js()) return;

if (env_->tick_callback_function()
->Call(env_->context(), process, 0, nullptr).IsEmpty()) {
Local<Function> tick_callback = env_->tick_callback_function();

// The tick is triggered before JS land calls SetTickCallback
// to initializes the tick callback during bootstrap.
CHECK(!tick_callback.IsEmpty());

if (tick_callback->Call(env_->context(), process, 0, nullptr).IsEmpty()) {
failed_ = true;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/env.cc
Expand Up @@ -241,6 +241,10 @@ Environment::Environment(IsolateData* isolate_data,
if (options_->no_force_async_hooks_checks) {
async_hooks_.no_force_checks();
}

// TODO(addaleax): the per-isolate state should not be controlled by
// a single Environment.
isolate()->SetPromiseRejectCallback(task_queue::PromiseRejectCallback);
}

Environment::~Environment() {
Expand Down
4 changes: 4 additions & 0 deletions src/node_internals.h
Expand Up @@ -180,6 +180,10 @@ SlicedArguments::SlicedArguments(
size_ = size;
}

namespace task_queue {
void PromiseRejectCallback(v8::PromiseRejectMessage message);
} // namespace task_queue

v8::Maybe<bool> ProcessEmitWarning(Environment* env, const char* fmt, ...);
v8::Maybe<bool> ProcessEmitDeprecationWarning(Environment* env,
const char* warning,
Expand Down
17 changes: 8 additions & 9 deletions src/node_task_queue.cc
Expand Up @@ -36,7 +36,7 @@ static void SetTickCallback(const FunctionCallbackInfo<Value>& args) {
env->set_tick_callback_function(args[0].As<Function>());
}

static void PromiseRejectCallback(PromiseRejectMessage message) {
void PromiseRejectCallback(PromiseRejectMessage message) {
static std::atomic<uint64_t> unhandledRejections{0};
static std::atomic<uint64_t> rejectionsHandledAfter{0};

Expand All @@ -49,6 +49,10 @@ static void PromiseRejectCallback(PromiseRejectMessage message) {
if (env == nullptr) return;

Local<Function> callback = env->promise_reject_callback();
// The promise is rejected before JS land calls SetPromiseRejectCallback
// to initializes the promise reject callback during bootstrap.
CHECK(!callback.IsEmpty());

Local<Value> value;
Local<Value> type = Number::New(env->isolate(), event);

Expand Down Expand Up @@ -83,17 +87,12 @@ static void PromiseRejectCallback(PromiseRejectMessage message) {
env->context(), Undefined(isolate), arraysize(args), args));
}

static void InitializePromiseRejectCallback(
static void SetPromiseRejectCallback(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

CHECK(args[0]->IsFunction());

// TODO(joyeecheung): this may be moved to somewhere earlier in the bootstrap
// to make sure it's only called once
isolate->SetPromiseRejectCallback(PromiseRejectCallback);

env->set_promise_reject_callback(args[0].As<Function>());
}

Expand All @@ -120,8 +119,8 @@ static void Initialize(Local<Object> target,
FIXED_ONE_BYTE_STRING(isolate, "promiseRejectEvents"),
events).FromJust();
env->SetMethod(target,
"initializePromiseRejectCallback",
InitializePromiseRejectCallback);
"setPromiseRejectCallback",
SetPromiseRejectCallback);
}

} // namespace task_queue
Expand Down
2 changes: 1 addition & 1 deletion test/message/events_unhandled_error_nexttick.out
Expand Up @@ -14,7 +14,7 @@ Error
at startExecution (internal/bootstrap/node.js:*:*)
Emitted 'error' event at:
at process.nextTick (*events_unhandled_error_nexttick.js:*:*)
at internalTickCallback (internal/process/next_tick.js:*:*)
at processTicksAndRejections (internal/process/next_tick.js:*:*)
at process.runNextTicks [as _tickCallback] (internal/process/next_tick.js:*:*)
at Function.Module.runMain (internal/modules/cjs/loader.js:*:*)
at executeUserCode (internal/bootstrap/node.js:*:*)
Expand Down
2 changes: 1 addition & 1 deletion test/message/nexttick_throw.out
Expand Up @@ -4,7 +4,7 @@
^
ReferenceError: undefined_reference_error_maker is not defined
at *test*message*nexttick_throw.js:*:*
at internalTickCallback (internal/process/next_tick.js:*:*)
at processTicksAndRejections (internal/process/next_tick.js:*:*)
at process.runNextTicks [as _tickCallback] (internal/process/next_tick.js:*:*)
at Function.Module.runMain (internal/modules/cjs/loader.js:*:*)
at executeUserCode (internal/bootstrap/node.js:*:*)
Expand Down
8 changes: 4 additions & 4 deletions test/message/stdin_messages.out
Expand Up @@ -12,7 +12,7 @@ SyntaxError: Strict mode code may not include a with statement
at Socket.process.stdin.on (internal/bootstrap/node.js:*:*)
at Socket.emit (events.js:*:*)
at endReadableNT (_stream_readable.js:*:*)
at process.internalTickCallback (internal/process/next_tick.js:*:*)
at processTicksAndRejections (internal/process/next_tick.js:*:*)
42
42
[stdin]:1
Expand All @@ -29,7 +29,7 @@ Error: hello
at Socket.process.stdin.on (internal/bootstrap/node.js:*:*)
at Socket.emit (events.js:*:*)
at endReadableNT (_stream_readable.js:*:*)
at process.internalTickCallback (internal/process/next_tick.js:*:*)
at processTicksAndRejections (internal/process/next_tick.js:*:*)
[stdin]:1
throw new Error("hello")
^
Expand All @@ -44,7 +44,7 @@ Error: hello
at Socket.process.stdin.on (internal/bootstrap/node.js:*:*)
at Socket.emit (events.js:*:*)
at endReadableNT (_stream_readable.js:*:*)
at process.internalTickCallback (internal/process/next_tick.js:*:*)
at processTicksAndRejections (internal/process/next_tick.js:*:*)
100
[stdin]:1
var x = 100; y = x;
Expand All @@ -60,7 +60,7 @@ ReferenceError: y is not defined
at Socket.process.stdin.on (internal/bootstrap/node.js:*:*)
at Socket.emit (events.js:*:*)
at endReadableNT (_stream_readable.js:*:*)
at process.internalTickCallback (internal/process/next_tick.js:*:*)
at processTicksAndRejections (internal/process/next_tick.js:*:*)

[stdin]:1
var ______________________________________________; throw 10
Expand Down

0 comments on commit f322d28

Please sign in to comment.