-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
src,async_hooks,n-api: refactor async callback handling #14697
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
Changes from 1 commit
eacd2e9
95976d8
f3ef344
4262fad
008d3bc
4a02025
733e29d
15aca60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Merge the two almost-but-not-quite identical `MakeCallback()` implementations - Provide a public `CallbackScope` class for embedders in order to enable `MakeCallback()`-like behaviour without tying that to calling a JS function PR-URL: #14697 Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,6 +161,7 @@ using v8::SealHandleScope; | |
| using v8::String; | ||
| using v8::TryCatch; | ||
| using v8::Uint32Array; | ||
| using v8::Undefined; | ||
| using v8::V8; | ||
| using v8::Value; | ||
|
|
||
|
|
@@ -1311,85 +1312,153 @@ void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) { | |
| env->AddPromiseHook(fn, arg); | ||
| } | ||
|
|
||
| class InternalCallbackScope { | ||
| public: | ||
| InternalCallbackScope(Environment* env, | ||
| Local<Object> object, | ||
| const async_context& asyncContext); | ||
| ~InternalCallbackScope(); | ||
| void Close(); | ||
|
|
||
| inline bool Failed() const { return failed_; } | ||
| inline void MarkAsFailed() { failed_ = true; } | ||
| inline bool IsInnerMakeCallback() const { | ||
| return callback_scope_.in_makecallback(); | ||
| } | ||
|
|
||
| private: | ||
| Environment* env_; | ||
| async_context async_context_; | ||
| v8::Local<v8::Object> object_; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't this only work if the class instance is wrapped in a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what you mean by wrapped, but yes, there needs to be a valid HandleScope for the object’s lifetime. That is already ensured because you can’t pass the |
||
| Environment::AsyncCallbackScope callback_scope_; | ||
| bool failed_ = false; | ||
| bool pushed_ids_ = false; | ||
| bool closed_ = false; | ||
| }; | ||
|
|
||
| MaybeLocal<Value> MakeCallback(Environment* env, | ||
| Local<Value> recv, | ||
| const Local<Function> callback, | ||
| int argc, | ||
| Local<Value> argv[], | ||
| async_context asyncContext) { | ||
| // If you hit this assertion, you forgot to enter the v8::Context first. | ||
| CHECK_EQ(env->context(), env->isolate()->GetCurrentContext()); | ||
| CallbackScope::CallbackScope(Isolate* isolate, | ||
| Local<Object> object, | ||
| async_context asyncContext) | ||
| : private_(new InternalCallbackScope(Environment::GetCurrent(isolate), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The lifetime of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intention here is that userland code won’t have access to the fields of Semantically, |
||
| object, | ||
| asyncContext)), | ||
| try_catch_(isolate) { | ||
| try_catch_.SetVerbose(true); | ||
| } | ||
|
|
||
| Local<Object> object; | ||
| CallbackScope::~CallbackScope() { | ||
| if (try_catch_.HasCaught()) | ||
| private_->MarkAsFailed(); | ||
| delete private_; | ||
| } | ||
|
|
||
| Environment::AsyncCallbackScope callback_scope(env); | ||
| bool disposed_domain = false; | ||
| InternalCallbackScope::InternalCallbackScope(Environment* env, | ||
| Local<Object> object, | ||
| const async_context& asyncContext) | ||
| : env_(env), | ||
| async_context_(asyncContext), | ||
| object_(object), | ||
| callback_scope_(env) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this probably stems from a lack of C++ knowledge, but why does passing
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| CHECK(!object.IsEmpty()); | ||
|
|
||
| if (recv->IsObject()) { | ||
| object = recv.As<Object>(); | ||
| } | ||
| // If you hit this assertion, you forgot to enter the v8::Context first. | ||
| CHECK_EQ(env->context(), env->isolate()->GetCurrentContext()); | ||
|
|
||
| if (env->using_domains()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The assumption here is that
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm? Not quite seeing that. The reverse assumption is present here, i.e. that if That assumption is already found in the previous code, and I do find it odd. I think we should either always or never do a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wish I could go back and take a screenshot of what I was seeing b/c the code I thought I wrote this comment for isn't there anymore. Nm me.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haha! I found it. Was from one of your previous commits: +InternalCallbackScope::InternalCallbackScope(Environment* env,
+ Local<Value> object,
+ const async_context& asyncContext)
+ : env_(env),
+ async_context_(asyncContext),
+ object_(object->IsObject() ? object.As<Object>() : Local<Object>()),
+ callback_scope_(env) {(I was specifically referring to assigning
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, right. :) |
||
| CHECK(recv->IsObject()); | ||
| disposed_domain = DomainEnter(env, object); | ||
| if (disposed_domain) return Undefined(env->isolate()); | ||
| failed_ = DomainEnter(env, object_); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But, like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Like, if you have suggestions that make sense across the board, I'm all for it. Or we could split this into different boolean values, but that might unnecessarily increase complexity.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what are the states? would a simple bit mask with a couple flags suffice?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jasnell Currently and previously, those are:
@jasnell Just to be clear, we would be setting different bits in a bit mask, but would never inspect those values, only care whether one was set. Am I understanding correctly that that's what you're suggesting?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So The reason the domain callbacks wrap the async wrap callbacks is that if the domain is disposed then |
||
| if (failed_) | ||
| return; | ||
| } | ||
|
|
||
| MaybeLocal<Value> ret; | ||
| if (asyncContext.async_id != 0) { | ||
| // No need to check a return value because the application will exit if | ||
| // an exception occurs. | ||
| AsyncWrap::EmitBefore(env, asyncContext.async_id); | ||
| } | ||
|
|
||
| { | ||
| AsyncHooks::ExecScope exec_scope(env, asyncContext.async_id, | ||
| asyncContext.trigger_async_id); | ||
| env->async_hooks()->push_ids(async_context_.async_id, | ||
| async_context_.trigger_async_id); | ||
| pushed_ids_ = true; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see how
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| if (asyncContext.async_id != 0) { | ||
| // No need to check a return value because the application will exit if | ||
| // an exception occurs. | ||
| AsyncWrap::EmitBefore(env, asyncContext.async_id); | ||
| } | ||
| InternalCallbackScope::~InternalCallbackScope() { | ||
| Close(); | ||
| } | ||
|
|
||
| ret = callback->Call(env->context(), recv, argc, argv); | ||
| void InternalCallbackScope::Close() { | ||
| if (closed_) return; | ||
| closed_ = true; | ||
|
|
||
| if (ret.IsEmpty()) { | ||
| // NOTE: For backwards compatibility with public API we return Undefined() | ||
| // if the top level call threw. | ||
| return callback_scope.in_makecallback() ? | ||
| ret : Undefined(env->isolate()); | ||
| } | ||
| if (pushed_ids_) | ||
| env_->async_hooks()->pop_ids(async_context_.async_id); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a patch for an error here. Will get you the commit later today.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what to say here, you can feel free to post the patch or let me know what the problem is (and whether it’s a new one).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry. that comment isn't a blocker. was a self-reminder b/c i thought the fix depended on this PR. |
||
|
|
||
| if (asyncContext.async_id != 0) { | ||
| AsyncWrap::EmitAfter(env, asyncContext.async_id); | ||
| } | ||
| if (failed_) return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to look at later: This may need to go before the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This matches the previous behaviour, and I think that’s okay because you already explicitly accounted for that situation in |
||
|
|
||
| if (async_context_.async_id != 0) { | ||
| AsyncWrap::EmitAfter(env_, async_context_.async_id); | ||
| } | ||
|
|
||
| if (env->using_domains()) { | ||
| disposed_domain = DomainExit(env, object); | ||
| if (disposed_domain) return Undefined(env->isolate()); | ||
| if (env_->using_domains()) { | ||
| failed_ = DomainExit(env_, object_); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like |
||
| if (failed_) return; | ||
| } | ||
|
|
||
| if (callback_scope.in_makecallback()) { | ||
| return ret; | ||
| if (IsInnerMakeCallback()) { | ||
| return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is purely my personal opinion of having balance between what runs entering and exiting the scope, but I feel that from here down should be put into it's own method (e.g. again, this isn't a technical issue, but it jumps at me every time I see it.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m not 100 % following – you mean, move this from |
||
| } | ||
|
|
||
| Environment::TickInfo* tick_info = env->tick_info(); | ||
| Environment::TickInfo* tick_info = env_->tick_info(); | ||
|
|
||
| if (tick_info->length() == 0) { | ||
| env->isolate()->RunMicrotasks(); | ||
| env_->isolate()->RunMicrotasks(); | ||
| } | ||
|
|
||
| // Make sure the stack unwound properly. If there are nested MakeCallback's | ||
| // then it should return early and not reach this code. | ||
| CHECK_EQ(env->current_async_id(), asyncContext.async_id); | ||
| CHECK_EQ(env->trigger_id(), asyncContext.trigger_async_id); | ||
| CHECK_EQ(env_->current_async_id(), 0); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: why is this correct? the stack unwinds completely?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, this is only executed for the bottom-most callback scope. |
||
| CHECK_EQ(env_->trigger_id(), 0); | ||
|
|
||
| Local<Object> process = env->process_object(); | ||
| Local<Object> process = env_->process_object(); | ||
|
|
||
| if (tick_info->length() == 0) { | ||
| tick_info->set_index(0); | ||
| return ret; | ||
| return; | ||
| } | ||
|
|
||
| CHECK_EQ(env_->current_async_id(), 0); | ||
| CHECK_EQ(env_->trigger_id(), 0); | ||
|
|
||
| if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) { | ||
| failed_ = true; | ||
| } | ||
| } | ||
|
|
||
| MaybeLocal<Value> InternalMakeCallback(Environment* env, | ||
| Local<Object> recv, | ||
| const Local<Function> callback, | ||
| int argc, | ||
| Local<Value> argv[], | ||
| async_context asyncContext) { | ||
| InternalCallbackScope scope(env, recv, asyncContext); | ||
| if (scope.Failed()) { | ||
| return Undefined(env->isolate()); | ||
| } | ||
|
|
||
| MaybeLocal<Value> ret; | ||
|
|
||
| { | ||
| ret = callback->Call(env->context(), recv, argc, argv); | ||
|
|
||
| if (ret.IsEmpty()) { | ||
| // NOTE: For backwards compatibility with public API we return Undefined() | ||
| // if the top level call threw. | ||
| scope.MarkAsFailed(); | ||
| return scope.IsInnerMakeCallback() ? ret : Undefined(env->isolate()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I question if this is necessary anymore if we're returning a It may be considered a semver-major, but looks like always returning
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why is that?
Yes,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That's what testing has shown. c++ file: double zid = 41;
static void Run(const v8::FunctionCallbackInfo<v8::Value>& args) {
auto isolate = args.GetIsolate();
auto fn = args[0].As<Function>();
auto obj = args[1].As<Object>();
MaybeLocal<Value> ret =
node::MakeCallback(isolate, obj, fn, 0, nullptr, { ++zid, ++zid });
fprintf(stderr, "IsEmpty(): %s\n", ret.IsEmpty() ? "true" : "false");
}js file: const release_type = process.config.target_defaults.default_configuration;
const addon = require(`./build/${release_type}/addon`);
const async_hooks = require('async_hooks');
const print = (...a) => require('fs').writeSync(1, require('util').format(...a));
function a() {
print(`a id: ${async_hooks.executionAsyncId()} ret: ${addon.run(b, {})}\n`);
}
function b() {
print(`b id: ${async_hooks.executionAsyncId()} ret: ${addon.run(c, {})}\n`);
}
function c() {
print(`c id: ${async_hooks.executionAsyncId()}\n`);
throw new Error('crap');
}
addon.run(a);output: And actually, even if the call is wrapped in a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When there is no JS stack below the call ;) #14922 adds a test for it. |
||
| } | ||
| } | ||
|
|
||
| if (env->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) { | ||
| scope.Close(); | ||
| if (scope.Failed()) { | ||
| return Undefined(env->isolate()); | ||
| } | ||
|
|
||
|
|
@@ -1442,8 +1511,8 @@ MaybeLocal<Value> MakeCallback(Isolate* isolate, | |
| // the two contexts need not be the same. | ||
| Environment* env = Environment::GetCurrent(callback->CreationContext()); | ||
| Context::Scope context_scope(env->context()); | ||
| return MakeCallback(env, recv.As<Value>(), callback, argc, argv, | ||
| asyncContext); | ||
| return InternalMakeCallback(env, recv, callback, | ||
| argc, argv, asyncContext); | ||
| } | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similar to the other comment, this would read better as
MarkAsDisposed().There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasnell @trevnorris This isn't called for disposed domains.