diff --git a/doc/api/domain.md b/doc/api/domain.md index eb04987f089f9b..4ada1c2ca0afb1 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/node.cc b/src/node.cc index abb570fb9eb966..8b061d7f401c2b 100644 --- a/src/node.cc +++ b/src/node.cc @@ -143,6 +143,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; @@ -1114,6 +1115,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); @@ -1153,9 +1206,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(); } 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); + })); + })); + })); +}