From b9e6032d0ea2774413144857521f7f2613010d2e Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Tue, 9 Dec 2014 05:10:44 +0100 Subject: [PATCH] async-wrap: add event hooks Call a user-defined callback at specific points in the lifetime of an asynchronous event. Which are on instantiation, just before/after the callback has been run. **If any of these callbacks throws an exception, there is no forgiveness or recovery. A message will be displayed and a core file dumped.** Currently these only tie into AsyncWrap, meaning no call to a hook callback will be made for timers or process.nextTick() events. Though those will be added in a future commit. Here are a few notes on how to make the hooks work: - The "this" of all event hook callbacks is the request object. - The zero field (kCallInitHook) of the flags object passed to setupHooks() must be set != 0 before the init callback will be called. - kCallInitHook only affects the calling of the init callback. If the request object has been run through the create callback it will always run the before/after callbacks. Regardless of kCallInitHook. - In the init callback the property "_asyncQueue" must be attached to the request object. e.g. function initHook() { this._asyncQueue = {}; } - DO NOT inspect the properties of the object in the init callback. Since the object is in the middle of being instantiated there are some cases when a getter is not complete, and doing so will cause Node to crash. PR-URL: https://github.com/joyent/node/pull/8110 Signed-off-by: Trevor Norris Reviewed-by: Fedor Indutny Reviewed-by: Alexis Campailla Reviewed-by: Julien Gilli --- src/async-wrap-inl.h | 36 ++++++++++++++++++++++++++++++++ src/async-wrap.cc | 49 ++++++++++++++++++++++++++++++++++++++++++++ src/async-wrap.h | 6 +++++- src/env-inl.h | 25 ++++++++++++++++++++++ src/env.h | 28 +++++++++++++++++++++++++ src/node.cc | 25 +++++++++++++++++++++- 6 files changed, 167 insertions(+), 2 deletions(-) diff --git a/src/async-wrap-inl.h b/src/async-wrap-inl.h index 73a177872e93d2..4dbb3a6cfc42bd 100644 --- a/src/async-wrap-inl.h +++ b/src/async-wrap-inl.h @@ -27,6 +27,7 @@ #include "base-object-inl.h" #include "env.h" #include "env-inl.h" +#include "node_internals.h" #include "util.h" #include "util-inl.h" #include "v8.h" @@ -38,7 +39,42 @@ inline AsyncWrap::AsyncWrap(Environment* env, ProviderType provider, AsyncWrap* parent) : BaseObject(env, object), + has_async_queue_(false), provider_type_(provider) { + // Check user controlled flag to see if the init callback should run. + if (!env->call_async_init_hook()) + return; + + // TODO(trevnorris): Until it's verified all passed object's are not weak, + // add a HandleScope to make sure there's no leak. + v8::HandleScope scope(env->isolate()); + + v8::Local parent_obj; + + v8::TryCatch try_catch; + + // If a parent value was sent then call its pre/post functions to let it know + // a conceptual "child" is being instantiated (e.g. that a server has + // received a connection). + if (parent != nullptr) { + parent_obj = parent->object(); + env->async_hooks_pre_function()->Call(parent_obj, 0, nullptr); + if (try_catch.HasCaught()) + FatalError("node::AsyncWrap::AsyncWrap", "parent pre hook threw"); + } + + env->async_hooks_init_function()->Call(object, 0, nullptr); + + if (try_catch.HasCaught()) + FatalError("node::AsyncWrap::AsyncWrap", "init hook threw"); + + has_async_queue_ = true; + + if (parent != nullptr) { + env->async_hooks_post_function()->Call(parent_obj, 0, nullptr); + if (try_catch.HasCaught()) + FatalError("node::AsyncWrap::AsyncWrap", "parent post hook threw"); + } } diff --git a/src/async-wrap.cc b/src/async-wrap.cc index 2990f923c4eb1d..3526512c6e0089 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -30,6 +30,7 @@ using v8::Context; using v8::Function; +using v8::FunctionCallbackInfo; using v8::Handle; using v8::HandleScope; using v8::Integer; @@ -38,9 +39,39 @@ using v8::Local; using v8::Object; using v8::TryCatch; using v8::Value; +using v8::kExternalUint32Array; namespace node { +static void SetupHooks(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + + CHECK(args[0]->IsObject()); + CHECK(args[1]->IsFunction()); + CHECK(args[2]->IsFunction()); + CHECK(args[3]->IsFunction()); + + // Attach Fields enum from Environment::AsyncHooks. + // Flags attached to this object are: + // - kCallInitHook (0): Tells the AsyncWrap constructor whether it should + // make a call to the init JS callback. This is disabled by default, so + // even after setting the callbacks the flag will have to be set to + // non-zero to have those callbacks called. This only affects the init + // callback. If the init callback was called, then the pre/post callbacks + // will automatically be called. + Local async_hooks_obj = args[0].As(); + Environment::AsyncHooks* async_hooks = env->async_hooks(); + async_hooks_obj->SetIndexedPropertiesToExternalArrayData( + async_hooks->fields(), + kExternalUint32Array, + async_hooks->fields_count()); + + env->set_async_hooks_init_function(args[1].As()); + env->set_async_hooks_pre_function(args[2].As()); + env->set_async_hooks_post_function(args[3].As()); +} + + static void Initialize(Handle target, Handle unused, Handle context) { @@ -48,6 +79,8 @@ static void Initialize(Handle target, Isolate* isolate = env->isolate(); HandleScope scope(isolate); + NODE_SET_METHOD(target, "setupHooks", SetupHooks); + Local async_providers = Object::New(isolate); #define V(PROVIDER) \ async_providers->Set(FIXED_ONE_BYTE_STRING(isolate, #PROVIDER), \ @@ -90,12 +123,28 @@ Handle AsyncWrap::MakeCallback(const Handle cb, } } + if (has_async_queue_) { + try_catch.SetVerbose(false); + env()->async_hooks_pre_function()->Call(context, 0, nullptr); + if (try_catch.HasCaught()) + FatalError("node::AsyncWrap::MakeCallback", "pre hook threw"); + try_catch.SetVerbose(true); + } + Local ret = cb->Call(context, argc, argv); if (try_catch.HasCaught()) { return Undefined(env()->isolate()); } + if (has_async_queue_) { + try_catch.SetVerbose(false); + env()->async_hooks_post_function()->Call(context, 0, nullptr); + if (try_catch.HasCaught()) + FatalError("node::AsyncWrap::MakeCallback", "post hook threw"); + try_catch.SetVerbose(true); + } + if (has_domain) { Local exit_v = domain->Get(env()->exit_string()); if (exit_v->IsFunction()) { diff --git a/src/async-wrap.h b/src/async-wrap.h index ea8dfd3f0122d0..0e42a00855c9a3 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -84,7 +84,11 @@ class AsyncWrap : public BaseObject { private: inline AsyncWrap(); - uint32_t provider_type_; + // When the async hooks init JS function is called from the constructor it is + // expected the context object will receive a _asyncQueue object property + // that will be used to call pre/post in MakeCallback. + bool has_async_queue_; + ProviderType provider_type_; }; } // namespace node diff --git a/src/env-inl.h b/src/env-inl.h index b40272cbdf0691..4ccf899a46e141 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -111,6 +111,22 @@ inline v8::Isolate* Environment::IsolateData::isolate() const { return isolate_; } +inline Environment::AsyncHooks::AsyncHooks() { + for (int i = 0; i < kFieldsCount; i++) fields_[i] = 0; +} + +inline uint32_t* Environment::AsyncHooks::fields() { + return fields_; +} + +inline int Environment::AsyncHooks::fields_count() const { + return kFieldsCount; +} + +inline bool Environment::AsyncHooks::call_init_hook() { + return fields_[kCallInitHook] != 0; +} + inline Environment::DomainFlag::DomainFlag() { for (int i = 0; i < kFieldsCount; ++i) fields_[i] = 0; } @@ -256,6 +272,11 @@ inline v8::Isolate* Environment::isolate() const { return isolate_; } +inline bool Environment::call_async_init_hook() const { + // The const_cast is okay, it doesn't violate conceptual const-ness. + return const_cast(this)->async_hooks()->call_init_hook(); +} + inline bool Environment::in_domain() const { // The const_cast is okay, it doesn't violate conceptual const-ness. return using_domains() && @@ -307,6 +328,10 @@ inline uv_loop_t* Environment::event_loop() const { return isolate_data()->event_loop(); } +inline Environment::AsyncHooks* Environment::async_hooks() { + return &async_hooks_; +} + inline Environment::DomainFlag* Environment::domain_flag() { return &domain_flag_; } diff --git a/src/env.h b/src/env.h index 68ec7224d24875..c167041e25cec6 100644 --- a/src/env.h +++ b/src/env.h @@ -65,6 +65,7 @@ namespace node { V(args_string, "args") \ V(argv_string, "argv") \ V(async, "async") \ + V(async_queue_string, "_asyncQueue") \ V(atime_string, "atime") \ V(birthtime_string, "birthtime") \ V(blksize_string, "blksize") \ @@ -249,6 +250,9 @@ namespace node { V(zero_return_string, "ZERO_RETURN") \ #define ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) \ + V(async_hooks_init_function, v8::Function) \ + V(async_hooks_pre_function, v8::Function) \ + V(async_hooks_post_function, v8::Function) \ V(binding_cache_object, v8::Object) \ V(buffer_constructor_function, v8::Function) \ V(context, v8::Context) \ @@ -282,6 +286,27 @@ RB_HEAD(ares_task_list, ares_task_t); class Environment { public: + class AsyncHooks { + public: + inline uint32_t* fields(); + inline int fields_count() const; + inline bool call_init_hook(); + + private: + friend class Environment; // So we can call the constructor. + inline AsyncHooks(); + + enum Fields { + // Set this to not zero if the init hook should be called. + kCallInitHook, + kFieldsCount + }; + + uint32_t fields_[kFieldsCount]; + + DISALLOW_COPY_AND_ASSIGN(AsyncHooks); + }; + class DomainFlag { public: inline uint32_t* fields(); @@ -373,6 +398,7 @@ class Environment { inline v8::Isolate* isolate() const; inline uv_loop_t* event_loop() const; + inline bool call_async_init_hook() const; inline bool in_domain() const; inline uint32_t watched_providers() const; @@ -392,6 +418,7 @@ class Environment { void *arg); inline void FinishHandleCleanup(uv_handle_t* handle); + inline AsyncHooks* async_hooks(); inline DomainFlag* domain_flag(); inline TickInfo* tick_info(); @@ -485,6 +512,7 @@ class Environment { uv_idle_t immediate_idle_handle_; uv_prepare_t idle_prepare_handle_; uv_check_t idle_check_handle_; + AsyncHooks async_hooks_; DomainFlag domain_flag_; TickInfo tick_info_; uv_timer_t cares_timer_handle_; diff --git a/src/node.cc b/src/node.cc index 130d5ab9ea36ab..cbc1f06c40db8d 100644 --- a/src/node.cc +++ b/src/node.cc @@ -996,11 +996,18 @@ Handle MakeCallback(Environment* env, Local process = env->process_object(); Local object, domain; + bool has_async_queue = false; bool has_domain = false; + if (recv->IsObject()) { + object = recv.As(); + Local async_queue_v = object->Get(env->async_queue_string()); + if (async_queue_v->IsObject()) + has_async_queue = true; + } + if (env->using_domains()) { CHECK(recv->IsObject()); - object = recv.As(); Local domain_v = object->Get(env->domain_string()); has_domain = domain_v->IsObject(); if (has_domain) { @@ -1022,8 +1029,24 @@ Handle MakeCallback(Environment* env, } } + if (has_async_queue) { + try_catch.SetVerbose(false); + env->async_hooks_pre_function()->Call(object, 0, nullptr); + if (try_catch.HasCaught()) + FatalError("node:;MakeCallback", "pre hook threw"); + try_catch.SetVerbose(true); + } + Local ret = callback->Call(recv, argc, argv); + if (has_async_queue) { + try_catch.SetVerbose(false); + env->async_hooks_post_function()->Call(object, 0, nullptr); + if (try_catch.HasCaught()) + FatalError("node::MakeCallback", "post hook threw"); + try_catch.SetVerbose(true); + } + if (has_domain) { Local exit_v = domain->Get(env->exit_string()); if (exit_v->IsFunction()) {