Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

domain: support promises #12489

Closed
wants to merge 10 commits into from
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
16 changes: 16 additions & 0 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<v8::Promise> promise,
v8::Local<v8::Value> parent) {
Environment* env = Environment::GetCurrent(promise->CreationContext());
for (const PromiseHookCallback& hook : env->promise_hooks_) {
hook.cb_(type, promise, parent, hook.arg_);
}
}

} // namespace node
13 changes: 13 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "util.h"
#include "uv.h"
#include "v8.h"
#include "node.h"

#include <list>
#include <stdint.h>
Expand Down Expand Up @@ -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<v8::Value> (*fun)(v8::Local<v8::String>),
const char* errmsg);
Expand Down Expand Up @@ -620,6 +623,16 @@ class Environment {
};
std::list<AtExitCallback> at_exit_functions_;

struct PromiseHookCallback {
promise_hook_func cb_;
void* arg_;
};
std::vector<PromiseHookCallback> promise_hooks_;

static void EnvPromiseHook(v8::PromiseHookType type,
v8::Local<v8::Promise> promise,
v8::Local<v8::Value> parent);

#define V(PropertyName, TypeName) \
v8::Persistent<TypeName> PropertyName ## _;
ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V)
Expand Down
62 changes: 62 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1113,6 +1114,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 @@ -1152,9 +1205,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 Expand Up @@ -1232,6 +1288,12 @@ void SetupPromises(const FunctionCallbackInfo<Value>& args) {
} // anonymous namespace


void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) {
Environment* env = Environment::GetCurrent(isolate);
env->AddPromiseHook(fn, arg);
}


Local<Value> MakeCallback(Environment* env,
Local<Value> recv,
const Local<Function> callback,
Expand Down
11 changes: 11 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<v8::Promise> promise,
v8::Local<v8::Value> 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_
6 changes: 6 additions & 0 deletions test/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -675,3 +675,9 @@ exports.getArrayBufferViews = function getArrayBufferViews(buf) {
}
return out;
};

// Crash the process on unhandled rejections.
exports.crashOnUnhandledRejection = function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation for this should be added to test/README.md.

process.on('unhandledRejection',
(err) => process.nextTick(() => { throw err; }));
};
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);
}));
}));
}));
}