async_wrap,src: promise hook integration
This change provides unified tracking of asynchronous promise lifecycles for both domains and async hooks. PR-URL: https://github.com/nodejs/node/pull/13000 Reviewed-By: Andreas Madsen <amwebdk@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
parent
410b141764
commit
920278bd37
@ -44,6 +44,8 @@ using v8::Local;
|
||||
using v8::MaybeLocal;
|
||||
using v8::Number;
|
||||
using v8::Object;
|
||||
using v8::Promise;
|
||||
using v8::PromiseHookType;
|
||||
using v8::RetainedObjectInfo;
|
||||
using v8::Symbol;
|
||||
using v8::TryCatch;
|
||||
@ -177,6 +179,143 @@ static void PushBackDestroyId(Environment* env, double id) {
|
||||
}
|
||||
|
||||
|
||||
bool DomainEnter(Environment* env, Local<Object> object) {
|
||||
Local<Value> domain_v = object->Get(env->domain_string());
|
||||
if (domain_v->IsObject()) {
|
||||
Local<Object> domain = domain_v.As<Object>();
|
||||
if (domain->Get(env->disposed_string())->IsTrue())
|
||||
return true;
|
||||
Local<Value> enter_v = domain->Get(env->enter_string());
|
||||
if (enter_v->IsFunction()) {
|
||||
if (enter_v.As<Function>()->Call(domain, 0, nullptr).IsEmpty()) {
|
||||
FatalError("node::AsyncWrap::MakeCallback",
|
||||
"domain enter callback threw, please report this");
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool DomainExit(Environment* env, v8::Local<v8::Object> object) {
|
||||
Local<Value> domain_v = object->Get(env->domain_string());
|
||||
if (domain_v->IsObject()) {
|
||||
Local<Object> domain = domain_v.As<Object>();
|
||||
if (domain->Get(env->disposed_string())->IsTrue())
|
||||
return true;
|
||||
Local<Value> exit_v = domain->Get(env->exit_string());
|
||||
if (exit_v->IsFunction()) {
|
||||
if (exit_v.As<Function>()->Call(domain, 0, nullptr).IsEmpty()) {
|
||||
FatalError("node::AsyncWrap::MakeCallback",
|
||||
"domain exit callback threw, please report this");
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static bool PreCallbackExecution(AsyncWrap* wrap, bool run_domain_cbs) {
|
||||
AsyncHooks* async_hooks = wrap->env()->async_hooks();
|
||||
|
||||
if (wrap->env()->using_domains() && run_domain_cbs) {
|
||||
bool is_disposed = DomainEnter(wrap->env(), wrap->object());
|
||||
if (is_disposed)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (async_hooks->fields()[AsyncHooks::kBefore] > 0) {
|
||||
Local<Value> uid = Number::New(wrap->env()->isolate(), wrap->get_id());
|
||||
Local<Function> fn = wrap->env()->async_hooks_before_function();
|
||||
TryCatch try_catch(wrap->env()->isolate());
|
||||
MaybeLocal<Value> ar = fn->Call(
|
||||
wrap->env()->context(), Undefined(wrap->env()->isolate()), 1, &uid);
|
||||
if (ar.IsEmpty()) {
|
||||
ClearFatalExceptionHandlers(wrap->env());
|
||||
FatalException(wrap->env()->isolate(), try_catch);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool PostCallbackExecution(AsyncWrap* wrap, bool run_domain_cbs) {
|
||||
AsyncHooks* async_hooks = wrap->env()->async_hooks();
|
||||
|
||||
// If the callback failed then the after() hooks will be called at the end
|
||||
// of _fatalException().
|
||||
if (async_hooks->fields()[AsyncHooks::kAfter] > 0) {
|
||||
Local<Value> uid = Number::New(wrap->env()->isolate(), wrap->get_id());
|
||||
Local<Function> fn = wrap->env()->async_hooks_after_function();
|
||||
TryCatch try_catch(wrap->env()->isolate());
|
||||
MaybeLocal<Value> ar = fn->Call(
|
||||
wrap->env()->context(), Undefined(wrap->env()->isolate()), 1, &uid);
|
||||
if (ar.IsEmpty()) {
|
||||
ClearFatalExceptionHandlers(wrap->env());
|
||||
FatalException(wrap->env()->isolate(), try_catch);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (wrap->env()->using_domains() && run_domain_cbs) {
|
||||
bool is_disposed = DomainExit(wrap->env(), wrap->object());
|
||||
if (is_disposed)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class PromiseWrap : public AsyncWrap {
|
||||
public:
|
||||
PromiseWrap(Environment* env, Local<Object> object)
|
||||
: AsyncWrap(env, object, PROVIDER_PROMISE) {}
|
||||
size_t self_size() const override { return sizeof(*this); }
|
||||
};
|
||||
|
||||
|
||||
static void PromiseHook(PromiseHookType type, Local<Promise> promise,
|
||||
Local<Value> parent, void* arg) {
|
||||
Local<Context> context = promise->CreationContext();
|
||||
Environment* env = Environment::GetCurrent(context);
|
||||
if (type == PromiseHookType::kInit) {
|
||||
// Unfortunately, promises don't have internal fields. Need a surrogate that
|
||||
// async wrap can wrap.
|
||||
Local<Object> obj =
|
||||
env->async_hooks_promise_object()->NewInstance(context).ToLocalChecked();
|
||||
PromiseWrap* wrap = new PromiseWrap(env, obj);
|
||||
v8::PropertyAttribute hidden =
|
||||
static_cast<v8::PropertyAttribute>(v8::ReadOnly
|
||||
| v8::DontDelete
|
||||
| v8::DontEnum);
|
||||
promise->DefineOwnProperty(context,
|
||||
env->promise_wrap(),
|
||||
v8::External::New(env->isolate(), wrap),
|
||||
hidden).FromJust();
|
||||
// The async tag will be destroyed at the same time as the promise as the
|
||||
// only reference to it is held by the promise. This allows the promise
|
||||
// wrap instance to be notified when the promise is destroyed.
|
||||
promise->DefineOwnProperty(context,
|
||||
env->promise_async_tag(),
|
||||
obj, hidden).FromJust();
|
||||
} else if (type == PromiseHookType::kResolve) {
|
||||
// TODO(matthewloring): need to expose this through the async hooks api.
|
||||
}
|
||||
Local<v8::Value> external_wrap =
|
||||
promise->Get(context, env->promise_wrap()).ToLocalChecked();
|
||||
PromiseWrap* wrap =
|
||||
static_cast<PromiseWrap*>(external_wrap.As<v8::External>()->Value());
|
||||
CHECK_NE(wrap, nullptr);
|
||||
if (type == PromiseHookType::kBefore) {
|
||||
PreCallbackExecution(wrap, false);
|
||||
} else if (type == PromiseHookType::kAfter) {
|
||||
PostCallbackExecution(wrap, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
|
||||
@ -201,6 +340,7 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
|
||||
SET_HOOK_FN(before);
|
||||
SET_HOOK_FN(after);
|
||||
SET_HOOK_FN(destroy);
|
||||
env->AddPromiseHook(PromiseHook, nullptr);
|
||||
#undef SET_HOOK_FN
|
||||
}
|
||||
|
||||
@ -262,6 +402,11 @@ void AsyncWrap::Initialize(Local<Object> target,
|
||||
env->SetMethod(target, "clearIdStack", ClearIdStack);
|
||||
env->SetMethod(target, "addIdToDestroyList", QueueDestroyId);
|
||||
|
||||
Local<v8::ObjectTemplate> promise_object_template =
|
||||
v8::ObjectTemplate::New(env->isolate());
|
||||
promise_object_template->SetInternalFieldCount(1);
|
||||
env->set_async_hooks_promise_object(promise_object_template);
|
||||
|
||||
v8::PropertyAttribute ReadOnlyDontDelete =
|
||||
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
|
||||
|
||||
@ -416,87 +561,30 @@ Local<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
|
||||
Local<Value>* argv) {
|
||||
CHECK(env()->context() == env()->isolate()->GetCurrentContext());
|
||||
|
||||
AsyncHooks* async_hooks = env()->async_hooks();
|
||||
Local<Object> context = object();
|
||||
Local<Object> domain;
|
||||
Local<Value> uid;
|
||||
bool has_domain = false;
|
||||
|
||||
Environment::AsyncCallbackScope callback_scope(env());
|
||||
|
||||
if (env()->using_domains()) {
|
||||
Local<Value> domain_v = context->Get(env()->domain_string());
|
||||
has_domain = domain_v->IsObject();
|
||||
if (has_domain) {
|
||||
domain = domain_v.As<Object>();
|
||||
if (domain->Get(env()->disposed_string())->IsTrue())
|
||||
return Local<Value>();
|
||||
}
|
||||
}
|
||||
Environment::AsyncHooks::ExecScope exec_scope(env(),
|
||||
get_id(),
|
||||
get_trigger_id());
|
||||
|
||||
if (has_domain) {
|
||||
Local<Value> enter_v = domain->Get(env()->enter_string());
|
||||
if (enter_v->IsFunction()) {
|
||||
if (enter_v.As<Function>()->Call(domain, 0, nullptr).IsEmpty()) {
|
||||
FatalError("node::AsyncWrap::MakeCallback",
|
||||
"domain enter callback threw, please report this");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Want currentId() to return the correct value from the callbacks.
|
||||
AsyncHooks::ExecScope exec_scope(env(), get_id(), get_trigger_id());
|
||||
|
||||
if (async_hooks->fields()[AsyncHooks::kBefore] > 0) {
|
||||
uid = Number::New(env()->isolate(), get_id());
|
||||
Local<Function> fn = env()->async_hooks_before_function();
|
||||
TryCatch try_catch(env()->isolate());
|
||||
MaybeLocal<Value> ar = fn->Call(
|
||||
env()->context(), Undefined(env()->isolate()), 1, &uid);
|
||||
if (ar.IsEmpty()) {
|
||||
ClearFatalExceptionHandlers(env());
|
||||
FatalException(env()->isolate(), try_catch);
|
||||
return Local<Value>();
|
||||
}
|
||||
if (!PreCallbackExecution(this, true)) {
|
||||
return Local<Value>();
|
||||
}
|
||||
|
||||
// Finally... Get to running the user's callback.
|
||||
MaybeLocal<Value> ret = cb->Call(env()->context(), context, argc, argv);
|
||||
MaybeLocal<Value> ret = cb->Call(env()->context(), object(), argc, argv);
|
||||
|
||||
Local<Value> ret_v;
|
||||
if (!ret.ToLocal(&ret_v)) {
|
||||
return Local<Value>();
|
||||
}
|
||||
|
||||
// If the callback failed then the after() hooks will be called at the end
|
||||
// of _fatalException().
|
||||
if (async_hooks->fields()[AsyncHooks::kAfter] > 0) {
|
||||
if (uid.IsEmpty())
|
||||
uid = Number::New(env()->isolate(), get_id());
|
||||
Local<Function> fn = env()->async_hooks_after_function();
|
||||
TryCatch try_catch(env()->isolate());
|
||||
MaybeLocal<Value> ar = fn->Call(
|
||||
env()->context(), Undefined(env()->isolate()), 1, &uid);
|
||||
if (ar.IsEmpty()) {
|
||||
ClearFatalExceptionHandlers(env());
|
||||
FatalException(env()->isolate(), try_catch);
|
||||
return Local<Value>();
|
||||
}
|
||||
if (!PostCallbackExecution(this, true)) {
|
||||
return Local<Value>();
|
||||
}
|
||||
|
||||
// The execution scope of the id and trigger_id only go this far.
|
||||
exec_scope.Dispose();
|
||||
|
||||
if (has_domain) {
|
||||
Local<Value> exit_v = domain->Get(env()->exit_string());
|
||||
if (exit_v->IsFunction()) {
|
||||
if (exit_v.As<Function>()->Call(domain, 0, nullptr).IsEmpty()) {
|
||||
FatalError("node::AsyncWrap::MakeCallback",
|
||||
"domain exit callback threw, please report this");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callback_scope.in_makecallback()) {
|
||||
return ret_v;
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ namespace node {
|
||||
V(PIPECONNECTWRAP) \
|
||||
V(PIPEWRAP) \
|
||||
V(PROCESSWRAP) \
|
||||
V(PROMISE) \
|
||||
V(QUERYWRAP) \
|
||||
V(SHUTDOWNWRAP) \
|
||||
V(SIGNALWRAP) \
|
||||
@ -132,6 +133,9 @@ class AsyncWrap : public BaseObject {
|
||||
|
||||
void LoadAsyncWrapperInfo(Environment* env);
|
||||
|
||||
bool DomainEnter(Environment* env, v8::Local<v8::Object> object);
|
||||
bool DomainExit(Environment* env, v8::Local<v8::Object> object);
|
||||
|
||||
} // namespace node
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
@ -195,6 +195,8 @@ namespace node {
|
||||
V(preference_string, "preference") \
|
||||
V(priority_string, "priority") \
|
||||
V(produce_cached_data_string, "produceCachedData") \
|
||||
V(promise_wrap, "_promise_async_wrap") \
|
||||
V(promise_async_tag, "_promise_async_wrap_tag") \
|
||||
V(raw_string, "raw") \
|
||||
V(read_host_object_string, "_readHostObject") \
|
||||
V(readable_string, "readable") \
|
||||
@ -256,6 +258,7 @@ namespace node {
|
||||
V(async_hooks_init_function, v8::Function) \
|
||||
V(async_hooks_before_function, v8::Function) \
|
||||
V(async_hooks_after_function, v8::Function) \
|
||||
V(async_hooks_promise_object, v8::ObjectTemplate) \
|
||||
V(binding_cache_object, v8::Object) \
|
||||
V(buffer_constructor_function, v8::Function) \
|
||||
V(buffer_prototype_object, v8::Object) \
|
||||
|
35
src/node.cc
35
src/node.cc
@ -1130,7 +1130,6 @@ void DomainPromiseHook(PromiseHookType type,
|
||||
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(),
|
||||
@ -1139,38 +1138,10 @@ void DomainPromiseHook(PromiseHookType type,
|
||||
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");
|
||||
}
|
||||
}
|
||||
DomainEnter(env, promise);
|
||||
} else if (type == PromiseHookType::kAfter) {
|
||||
DomainExit(env, promise);
|
||||
}
|
||||
}
|
||||
|
||||
|
52
test/async-hooks/test-promise.js
Normal file
52
test/async-hooks/test-promise.js
Normal file
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const initHooks = require('./init-hooks');
|
||||
const { checkInvocations } = require('./hook-checks');
|
||||
|
||||
const hooks = initHooks();
|
||||
|
||||
hooks.enable();
|
||||
|
||||
const p = (new Promise(common.mustCall(executor)));
|
||||
p.then(afterresolution);
|
||||
|
||||
function executor(resolve, reject) {
|
||||
const as = hooks.activitiesOfTypes('PROMISE');
|
||||
assert.strictEqual(as.length, 1, 'one activities');
|
||||
const a = as[0];
|
||||
checkInvocations(a, { init: 1 }, 'while in promise executor');
|
||||
resolve(5);
|
||||
}
|
||||
|
||||
function afterresolution(val) {
|
||||
assert.strictEqual(val, 5);
|
||||
const as = hooks.activitiesOfTypes('PROMISE');
|
||||
assert.strictEqual(as.length, 2, 'two activities');
|
||||
checkInvocations(as[0], { init: 1 }, 'after resolution parent promise');
|
||||
checkInvocations(as[1], { init: 1, before: 1 },
|
||||
'after resolution child promise');
|
||||
}
|
||||
|
||||
process.on('exit', onexit);
|
||||
function onexit() {
|
||||
hooks.disable();
|
||||
hooks.sanityCheck('PROMISE');
|
||||
|
||||
const as = hooks.activitiesOfTypes('PROMISE');
|
||||
assert.strictEqual(as.length, 2, 'two activities');
|
||||
|
||||
const a0 = as[0];
|
||||
assert.strictEqual(a0.type, 'PROMISE', 'promise request');
|
||||
assert.strictEqual(typeof a0.uid, 'number', 'uid is a number');
|
||||
assert.strictEqual(a0.triggerId, 1, 'parent uid 1');
|
||||
checkInvocations(a0, { init: 1 }, 'when process exits');
|
||||
|
||||
const a1 = as[1];
|
||||
assert.strictEqual(a1.type, 'PROMISE', 'promise request');
|
||||
assert.strictEqual(typeof a1.uid, 'number', 'uid is a number');
|
||||
assert.strictEqual(a1.triggerId, 1, 'parent uid 1');
|
||||
// We expect a destroy hook as well but we cannot guarentee predictable gc.
|
||||
checkInvocations(a1, { init: 1, before: 1, after: 1 }, 'when process exits');
|
||||
}
|
@ -66,6 +66,14 @@ function testInitialized(req, ctor_name) {
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
// We don't want to expose getAsyncId for promises but we need to construct
|
||||
// one so that the cooresponding provider type is removed from the
|
||||
// providers list.
|
||||
new Promise((res) => res(5));
|
||||
}
|
||||
|
||||
|
||||
if (common.hasCrypto) {
|
||||
const tls = require('tls');
|
||||
// SecurePair
|
||||
@ -137,7 +145,6 @@ if (common.hasCrypto) {
|
||||
testInitialized(new Process(), 'Process');
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
const Signal = process.binding('signal_wrap').Signal;
|
||||
testInitialized(new Signal(), 'Signal');
|
||||
|
Loading…
x
Reference in New Issue
Block a user