diff --git a/doc/api/domain.md b/doc/api/domain.md index 00cb149bbd9f40..b85e6096b23ebf 100644 --- a/doc/api/domain.md +++ b/doc/api/domain.md @@ -1,4 +1,11 @@ # Domain + > Stability: 0 - Deprecated @@ -444,6 +451,49 @@ d.run(() => { In this example, the `d.on('error')` handler will be triggered, rather than crashing the program. +## Domains and Promises + +As of Node REPLACEME, the handlers of Promises are run inside the domain in +which the call to `.then` or `.catch` itself was made: + +```js +const d1 = domain.create(); +const d2 = domain.create(); + +let p; +d1.run(() => { + p = Promise.resolve(42); +}); + +d2.run(() => { + p.then((v) => { + // running in d2 + }); +}); +``` + +A callback may be bound to a specific domain using [`domain.bind(callback)`][]: + +```js +const d1 = domain.create(); +const d2 = domain.create(); + +let p; +d1.run(() => { + p = Promise.resolve(42); +}); + +d2.run(() => { + p.then(p.domain.bind((v) => { + // running in d1 + })); +}); +``` + +Note that domains will not interfere with the error handling mechanisms for +Promises, i.e. no `error` event will be emitted for unhandled Promise +rejections. + [`domain.add(emitter)`]: #domain_domain_add_emitter [`domain.bind(callback)`]: #domain_domain_bind_callback [`domain.dispose()`]: #domain_domain_dispose diff --git a/src/env.cc b/src/env.cc index b44b435d4e3256..034625b375446c 100644 --- a/src/env.cc +++ b/src/env.cc @@ -188,4 +188,20 @@ void Environment::AtExit(void (*cb)(void* arg), void* arg) { at_exit_functions_.push_back(AtExitCallback{cb, arg}); } +void Environment::AddPromiseHook(promise_hook_func fn, void* arg) { + promise_hooks_.push_back(PromiseHookCallback{fn, arg}); + if (promise_hooks_.size() == 1) { + isolate_->SetPromiseHook(EnvPromiseHook); + } +} + +void Environment::EnvPromiseHook(v8::PromiseHookType type, + v8::Local promise, + v8::Local parent) { + Environment* env = Environment::GetCurrent(promise->CreationContext()); + for (const PromiseHookCallback& hook : env->promise_hooks_) { + hook.cb_(type, promise, parent, hook.arg_); + } +} + } // namespace node diff --git a/src/env.h b/src/env.h index 8b158728a9a261..abf5f44de052df 100644 --- a/src/env.h +++ b/src/env.h @@ -35,6 +35,7 @@ #include "util.h" #include "uv.h" #include "v8.h" +#include "node.h" #include #include @@ -572,6 +573,8 @@ class Environment { static const int kContextEmbedderDataIndex = NODE_CONTEXT_EMBEDDER_DATA_INDEX; + void AddPromiseHook(promise_hook_func fn, void* arg); + private: inline void ThrowError(v8::Local (*fun)(v8::Local), const char* errmsg); @@ -620,6 +623,16 @@ class Environment { }; std::list at_exit_functions_; + struct PromiseHookCallback { + promise_hook_func cb_; + void* arg_; + }; + std::vector promise_hooks_; + + static void EnvPromiseHook(v8::PromiseHookType type, + v8::Local promise, + v8::Local parent); + #define V(PropertyName, TypeName) \ v8::Persistent PropertyName ## _; ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) diff --git a/src/node.cc b/src/node.cc index 748e1ee4f32e12..5a47b083a8b82a 100644 --- a/src/node.cc +++ b/src/node.cc @@ -142,6 +142,7 @@ using v8::Number; using v8::Object; using v8::ObjectTemplate; using v8::Promise; +using v8::PromiseHookType; using v8::PromiseRejectMessage; using v8::PropertyCallbackInfo; using v8::ScriptOrigin; @@ -1113,6 +1114,58 @@ bool ShouldAbortOnUncaughtException(Isolate* isolate) { } +void DomainPromiseHook(PromiseHookType type, + Local promise, + Local parent, + void* arg) { + Environment* env = static_cast(arg); + Local context = env->context(); + + if (type == PromiseHookType::kResolve) return; + if (type == PromiseHookType::kInit && env->in_domain()) { + promise->Set(context, + env->domain_string(), + env->domain_array()->Get(context, + 0).ToLocalChecked()).FromJust(); + return; + } + + // Loosely based on node::MakeCallback(). + Local domain_v = + promise->Get(context, env->domain_string()).ToLocalChecked(); + if (!domain_v->IsObject()) + return; + + Local domain = domain_v.As(); + if (domain->Get(context, env->disposed_string()) + .ToLocalChecked()->IsTrue()) { + return; + } + + if (type == PromiseHookType::kBefore) { + Local enter_v = + domain->Get(context, env->enter_string()).ToLocalChecked(); + if (enter_v->IsFunction()) { + if (enter_v.As()->Call(context, domain, 0, nullptr).IsEmpty()) { + FatalError("node::PromiseHook", + "domain enter callback threw, please report this " + "as a bug in Node.js"); + } + } + } else { + Local exit_v = + domain->Get(context, env->exit_string()).ToLocalChecked(); + if (exit_v->IsFunction()) { + if (exit_v.As()->Call(context, domain, 0, nullptr).IsEmpty()) { + FatalError("node::MakeCallback", + "domain exit callback threw, please report this " + "as a bug in Node.js"); + } + } + } +} + + void SetupDomainUse(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1152,9 +1205,12 @@ void SetupDomainUse(const FunctionCallbackInfo& args) { Local array_buffer = ArrayBuffer::New(env->isolate(), fields, sizeof(*fields) * fields_count); + env->AddPromiseHook(DomainPromiseHook, static_cast(env)); + args.GetReturnValue().Set(Uint32Array::New(array_buffer, 0, fields_count)); } + void RunMicrotasks(const FunctionCallbackInfo& args) { args.GetIsolate()->RunMicrotasks(); } @@ -1232,6 +1288,12 @@ void SetupPromises(const FunctionCallbackInfo& args) { } // anonymous namespace +void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) { + Environment* env = Environment::GetCurrent(isolate); + env->AddPromiseHook(fn, arg); +} + + Local MakeCallback(Environment* env, Local recv, const Local callback, diff --git a/src/node.h b/src/node.h index 4452b9d578bc1c..feb2c800798e8d 100644 --- a/src/node.h +++ b/src/node.h @@ -517,6 +517,17 @@ NODE_EXTERN void AtExit(void (*cb)(void* arg), void* arg = 0); */ NODE_EXTERN void AtExit(Environment* env, void (*cb)(void* arg), void* arg = 0); +typedef void (*promise_hook_func) (v8::PromiseHookType type, + v8::Local promise, + v8::Local parent, + void* arg); + +/* Registers an additional v8::PromiseHook wrapper. This API exists because V8 + * itself supports only a single PromiseHook. */ +NODE_EXTERN void AddPromiseHook(v8::Isolate* isolate, + promise_hook_func fn, + void* arg); + } // namespace node #endif // SRC_NODE_H_ diff --git a/test/common.js b/test/common.js index 6fe2d4520f6a08..3af9dda7f6c133 100644 --- a/test/common.js +++ b/test/common.js @@ -675,3 +675,9 @@ exports.getArrayBufferViews = function getArrayBufferViews(buf) { } return out; }; + +// Crash the process on unhandled rejections. +exports.crashOnUnhandledRejection = function() { + process.on('unhandledRejection', + (err) => process.nextTick(() => { throw err; })); +}; diff --git a/test/parallel/test-domain-promise.js b/test/parallel/test-domain-promise.js new file mode 100644 index 00000000000000..8bae75eb63b76a --- /dev/null +++ b/test/parallel/test-domain-promise.js @@ -0,0 +1,128 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const domain = require('domain'); +const fs = require('fs'); +const vm = require('vm'); + +common.crashOnUnhandledRejection(); + +{ + const d = domain.create(); + + d.run(common.mustCall(() => { + Promise.resolve().then(common.mustCall(() => { + assert.strictEqual(process.domain, d); + })); + })); +} + +{ + const d = domain.create(); + + d.run(common.mustCall(() => { + Promise.resolve().then(() => {}).then(() => {}).then(common.mustCall(() => { + assert.strictEqual(process.domain, d); + })); + })); +} + +{ + const d = domain.create(); + + d.run(common.mustCall(() => { + vm.runInNewContext(`Promise.resolve().then(common.mustCall(() => { + assert.strictEqual(process.domain, d); + }));`, { common, assert, process, d }); + })); +} + +{ + const d1 = domain.create(); + const d2 = domain.create(); + let p; + d1.run(common.mustCall(() => { + p = Promise.resolve(42); + })); + + d2.run(common.mustCall(() => { + p.then(common.mustCall((v) => { + assert.strictEqual(process.domain, d2); + assert.strictEqual(p.domain, d1); + })); + })); +} + +{ + const d1 = domain.create(); + const d2 = domain.create(); + let p; + d1.run(common.mustCall(() => { + p = Promise.resolve(42); + })); + + d2.run(common.mustCall(() => { + p.then(p.domain.bind(common.mustCall((v) => { + assert.strictEqual(process.domain, d1); + assert.strictEqual(p.domain, d1); + }))); + })); +} + +{ + const d1 = domain.create(); + const d2 = domain.create(); + let p; + d1.run(common.mustCall(() => { + p = Promise.resolve(42); + })); + + d1.run(common.mustCall(() => { + d2.run(common.mustCall(() => { + p.then(common.mustCall((v) => { + assert.strictEqual(process.domain, d2); + assert.strictEqual(p.domain, d1); + })); + })); + })); +} + +{ + const d1 = domain.create(); + const d2 = domain.create(); + let p; + d1.run(common.mustCall(() => { + p = Promise.reject(new Error('foobar')); + })); + + d2.run(common.mustCall(() => { + p.catch(common.mustCall((v) => { + assert.strictEqual(process.domain, d2); + assert.strictEqual(p.domain, d1); + })); + })); +} + +{ + const d = domain.create(); + + d.run(common.mustCall(() => { + Promise.resolve().then(common.mustCall(() => { + setTimeout(common.mustCall(() => { + assert.strictEqual(process.domain, d); + }), 0); + })); + })); +} + +{ + const d = domain.create(); + + d.run(common.mustCall(() => { + Promise.resolve().then(common.mustCall(() => { + fs.readFile(__filename, common.mustCall(() => { + assert.strictEqual(process.domain, d); + })); + })); + })); +}