diff --git a/src/async-wrap.cc b/src/async-wrap.cc index 06567d6f7ccf39..d3776c73334b49 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -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) { + Local domain_v = object->Get(env->domain_string()); + if (domain_v->IsObject()) { + Local domain = domain_v.As(); + if (domain->Get(env->disposed_string())->IsTrue()) + return true; + Local enter_v = domain->Get(env->enter_string()); + if (enter_v->IsFunction()) { + if (enter_v.As()->Call(domain, 0, nullptr).IsEmpty()) { + FatalError("node::AsyncWrap::MakeCallback", + "domain enter callback threw, please report this"); + } + } + } + return false; +} + + +bool DomainExit(Environment* env, v8::Local object) { + Local domain_v = object->Get(env->domain_string()); + if (domain_v->IsObject()) { + Local domain = domain_v.As(); + if (domain->Get(env->disposed_string())->IsTrue()) + return true; + Local exit_v = domain->Get(env->exit_string()); + if (exit_v->IsFunction()) { + if (exit_v.As()->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 uid = Number::New(wrap->env()->isolate(), wrap->get_id()); + Local fn = wrap->env()->async_hooks_before_function(); + TryCatch try_catch(wrap->env()->isolate()); + MaybeLocal 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 uid = Number::New(wrap->env()->isolate(), wrap->get_id()); + Local fn = wrap->env()->async_hooks_after_function(); + TryCatch try_catch(wrap->env()->isolate()); + MaybeLocal 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) + : AsyncWrap(env, object, PROVIDER_PROMISE) {} + size_t self_size() const override { return sizeof(*this); } +}; + + +static void PromiseHook(PromiseHookType type, Local promise, + Local parent, void* arg) { + Local 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 obj = + env->async_hooks_promise_object()->NewInstance(context).ToLocalChecked(); + PromiseWrap* wrap = new PromiseWrap(env, obj); + v8::PropertyAttribute hidden = + static_cast(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 external_wrap = + promise->Get(context, env->promise_wrap()).ToLocalChecked(); + PromiseWrap* wrap = + static_cast(external_wrap.As()->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& args) { Environment* env = Environment::GetCurrent(args); @@ -201,6 +340,7 @@ static void SetupHooks(const FunctionCallbackInfo& 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 target, env->SetMethod(target, "clearIdStack", ClearIdStack); env->SetMethod(target, "addIdToDestroyList", QueueDestroyId); + Local 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::ReadOnly | v8::DontDelete); @@ -416,87 +561,30 @@ Local AsyncWrap::MakeCallback(const Local cb, Local* argv) { CHECK(env()->context() == env()->isolate()->GetCurrentContext()); - AsyncHooks* async_hooks = env()->async_hooks(); - Local context = object(); - Local domain; - Local uid; - bool has_domain = false; - Environment::AsyncCallbackScope callback_scope(env()); - if (env()->using_domains()) { - Local domain_v = context->Get(env()->domain_string()); - has_domain = domain_v->IsObject(); - if (has_domain) { - domain = domain_v.As(); - if (domain->Get(env()->disposed_string())->IsTrue()) - return Local(); - } - } + Environment::AsyncHooks::ExecScope exec_scope(env(), + get_id(), + get_trigger_id()); - if (has_domain) { - Local enter_v = domain->Get(env()->enter_string()); - if (enter_v->IsFunction()) { - if (enter_v.As()->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 fn = env()->async_hooks_before_function(); - TryCatch try_catch(env()->isolate()); - MaybeLocal ar = fn->Call( - env()->context(), Undefined(env()->isolate()), 1, &uid); - if (ar.IsEmpty()) { - ClearFatalExceptionHandlers(env()); - FatalException(env()->isolate(), try_catch); - return Local(); - } + if (!PreCallbackExecution(this, true)) { + return Local(); } // Finally... Get to running the user's callback. - MaybeLocal ret = cb->Call(env()->context(), context, argc, argv); + MaybeLocal ret = cb->Call(env()->context(), object(), argc, argv); Local ret_v; if (!ret.ToLocal(&ret_v)) { return Local(); } - // 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 fn = env()->async_hooks_after_function(); - TryCatch try_catch(env()->isolate()); - MaybeLocal ar = fn->Call( - env()->context(), Undefined(env()->isolate()), 1, &uid); - if (ar.IsEmpty()) { - ClearFatalExceptionHandlers(env()); - FatalException(env()->isolate(), try_catch); - return Local(); - } + if (!PostCallbackExecution(this, true)) { + return Local(); } - // The execution scope of the id and trigger_id only go this far. exec_scope.Dispose(); - if (has_domain) { - Local exit_v = domain->Get(env()->exit_string()); - if (exit_v->IsFunction()) { - if (exit_v.As()->Call(domain, 0, nullptr).IsEmpty()) { - FatalError("node::AsyncWrap::MakeCallback", - "domain exit callback threw, please report this"); - } - } - } - if (callback_scope.in_makecallback()) { return ret_v; } diff --git a/src/async-wrap.h b/src/async-wrap.h index eb03997b2c9467..cf81178747ae15 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -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 object); +bool DomainExit(Environment* env, v8::Local object); + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/env.h b/src/env.h index c8c8232cc07fd4..11c957d2be135b 100644 --- a/src/env.h +++ b/src/env.h @@ -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) \ diff --git a/src/node.cc b/src/node.cc index 9c44c56fc43f23..85b4b1264a5bcc 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1130,7 +1130,6 @@ void DomainPromiseHook(PromiseHookType type, 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(), @@ -1139,38 +1138,10 @@ void DomainPromiseHook(PromiseHookType type, 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"); - } - } + DomainEnter(env, promise); + } else if (type == PromiseHookType::kAfter) { + DomainExit(env, promise); } } diff --git a/test/async-hooks/test-promise.js b/test/async-hooks/test-promise.js new file mode 100644 index 00000000000000..30cf94d28b15ab --- /dev/null +++ b/test/async-hooks/test-promise.js @@ -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'); +} diff --git a/test/parallel/test-async-wrap-getasyncid.js b/test/parallel/test-async-wrap-getasyncid.js index 98aa3c857a8b18..6b9d33b8ca25c7 100644 --- a/test/parallel/test-async-wrap-getasyncid.js +++ b/test/parallel/test-async-wrap-getasyncid.js @@ -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');