Skip to content

Commit

Permalink
domain: support promises
Browse files Browse the repository at this point in the history
Fixes: #10724
PR-URL: #12489
Reviewed-By: Matthew Loring <mattloring@google.com>
Reviewed-By: Julien Gilli <jgilli@nodejs.org>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
  • Loading branch information
addaleax committed Apr 27, 2017
1 parent e5a25cb commit 84dabe8
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 0 deletions.
50 changes: 50 additions & 0 deletions doc/api/domain.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Domain
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/12489
description: Handlers for `Promise`s are now invoked in the domain in which
the first promise of a chain was created.
-->

> Stability: 0 - Deprecated
Expand Down Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1114,6 +1115,58 @@ bool ShouldAbortOnUncaughtException(Isolate* isolate) {
}


void DomainPromiseHook(PromiseHookType type,
Local<Promise> promise,
Local<Value> parent,
void* arg) {
Environment* env = static_cast<Environment*>(arg);
Local<Context> 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<Value> domain_v =
promise->Get(context, env->domain_string()).ToLocalChecked();
if (!domain_v->IsObject())
return;

Local<Object> domain = domain_v.As<Object>();
if (domain->Get(context, env->disposed_string())
.ToLocalChecked()->IsTrue()) {
return;
}

if (type == PromiseHookType::kBefore) {
Local<Value> enter_v =
domain->Get(context, env->enter_string()).ToLocalChecked();
if (enter_v->IsFunction()) {
if (enter_v.As<Function>()->Call(context, domain, 0, nullptr).IsEmpty()) {
FatalError("node::PromiseHook",
"domain enter callback threw, please report this "
"as a bug in Node.js");
}
}
} else {
Local<Value> exit_v =
domain->Get(context, env->exit_string()).ToLocalChecked();
if (exit_v->IsFunction()) {
if (exit_v.As<Function>()->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<Value>& args) {
Environment* env = Environment::GetCurrent(args);

Expand Down Expand Up @@ -1153,9 +1206,12 @@ void SetupDomainUse(const FunctionCallbackInfo<Value>& args) {
Local<ArrayBuffer> array_buffer =
ArrayBuffer::New(env->isolate(), fields, sizeof(*fields) * fields_count);

env->AddPromiseHook(DomainPromiseHook, static_cast<void*>(env));

args.GetReturnValue().Set(Uint32Array::New(array_buffer, 0, fields_count));
}


void RunMicrotasks(const FunctionCallbackInfo<Value>& args) {
args.GetIsolate()->RunMicrotasks();
}
Expand Down
128 changes: 128 additions & 0 deletions test/parallel/test-domain-promise.js
Original file line number Diff line number Diff line change
@@ -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);
}));
}));
}));
}

0 comments on commit 84dabe8

Please sign in to comment.