-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
async_hooks: proper id stacking for Promises #13585
Conversation
async_hooks.createHook({ | ||
init: common.mustCallAtLeast((id, type, triggerId) => { | ||
if (type === 'PROMISE') { | ||
assert.strictEqual(promiseAsyncIds[promiseAsyncIds.length - 1] || 1, |
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.
why test that the triggerId propagates correctly? Maybe you were thinking of testing async_hooks.triggerId()
?
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.
Nah, this is just because it seemed easy to add an assertion for. Do you think I should drop it / test other things as well?
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.
I found the assert hard to read, so either add a comment or remove it. You should definitely check that async_hooks.triggerId()
is correct, since that is the second parameter in push_ids
.
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.
I think this could have bad side-effects in combination with #13509. Consider:
Promise.resolve(1).then(function () {
// something that implicitly requires `async_hooks` and enables PromiseHooks
process.nextTick(function () {
});
// At this point, PromiseHooks will pop the id stack, but nothing will have been
// pushed because PromiseHooks was enabled late.
});
@AndreasMadsen Good point, I’ll take care of that. Give me a minute. :) |
553773b
to
931ab11
Compare
@AndreasMadsen You are right. I’ve rebased this on #13509 (which in turn is rebased on master) … I had to do some clever¹ stuff to get things working, but I think this makes sense and is consistent. ¹ edit to clarify: the bad kind of clever. ;) |
931ab11
to
f0344ef
Compare
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.
LGTM. If you wouldn't mind adding more detail to the git commit message about what it is you're doing. And you may need to fix the timing on some of the tests after #13509 lands.
324731d
to
9718b5f
Compare
@trevnorris Okay, updated. The commit message is a bit more descriptive now, and the tests should pass now that #13509 has landed. |
// Check that internal fields are no longer being set. This needs to be delayed | ||
// a bit because the `disable()` call only schedules disabling the hook in a | ||
// future microtask. | ||
setImmediate(() => { |
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.
Oh, interesting. Is this something we should fix? With for example
if (env->async_hooks()->fields()[AsyncHooks::kTotals])
return;
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.
We can’t because we need the pop_ids()
call to happen, so we need the PromiseHook
working until at least the kAfter
occurs.
This shouldn’t be a problem, it’s just emitting events for nobody to witness.
} | ||
|
||
|
||
static void DisablePromiseHook(const FunctionCallbackInfo<Value>& args) { | ||
Environment* env = Environment::GetCurrent(args); | ||
env->RemovePromiseHook(PromiseHook, nullptr); | ||
|
||
// Delay the call to `RemovePromiseHook` because we might currently be |
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.
Since we are going with the temp fix, I guess this is okay. But I really want to see a better solution. I'm not really convinced there aren't some obscure bug with the right .enable()
and .disable()
calls.
Environment* env = Environment::GetCurrent(context); | ||
|
||
// PromiseHook() should never be called if no hooks have been enabled. | ||
CHECK_GT(env->async_hooks()->fields()[AsyncHooks::kTotals], 0); |
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.
Please don't loose this check.
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.
@trevnorris I know why you put it there, and I am not a fan of removing it, but do you have a better suggestion?
It’s good the cctest is there, it basically checks the same thing, right?
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.
Ah yup. See now why it must to be removed. Nm.
@addaleax Thanks for working on this. I'm running a couple tests now and will get back with you as soon as I'm done. |
@addaleax Your patch gave me an idea. That AsyncWrap remove PromiseHook callbackdiff --git a/lib/async_hooks.js b/lib/async_hooks.js
index 53ce038..7deb5ed 100644
--- a/lib/async_hooks.js
+++ b/lib/async_hooks.js
@@ -139,8 +139,8 @@ class AsyncHook {
hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
hooks_array.splice(index, 1);
- if (prev_kTotals > 0 && hook_fields[kTotals] === 0)
- async_wrap.disablePromiseHook();
+ // Disabling of PromiseHook() will happen automatically at the end of
+ // synchronous execution.
return this;
}
diff --git a/src/async-wrap.cc b/src/async-wrap.cc
index 327fe36..41eba0f 100644
--- a/src/async-wrap.cc
+++ b/src/async-wrap.cc
@@ -437,15 +437,10 @@ static void EnablePromiseHook(const FunctionCallbackInfo<Value>& args) {
}
-static void DisablePromiseHook(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
-
- // Delay the call to `RemovePromiseHook` because we might currently be
- // between the `before` and `after` calls of a Promise.
- env->isolate()->EnqueueMicrotask([](void* data) {
- Environment* env = static_cast<Environment*>(data);
- env->RemovePromiseHook(PromiseHook, data);
- }, static_cast<void*>(env));
+void AsyncWrap::CheckAndRemovePromiseHook(Environment* env) {
+ if (env->async_hooks()->fields()[AsyncHooks::kTotals] == 0) {
+ env->RemovePromiseHook(PromiseHook, static_cast<void*>(env));
+ }
}
@@ -506,7 +501,6 @@ void AsyncWrap::Initialize(Local<Object> target,
env->SetMethod(target, "clearIdStack", ClearIdStack);
env->SetMethod(target, "addIdToDestroyList", QueueDestroyId);
env->SetMethod(target, "enablePromiseHook", EnablePromiseHook);
- env->SetMethod(target, "disablePromiseHook", DisablePromiseHook);
v8::PropertyAttribute ReadOnlyDontDelete =
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
@@ -691,6 +685,8 @@ Local<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
// Finally... Get to running the user's callback.
MaybeLocal<Value> ret = cb->Call(env()->context(), object(), argc, argv);
+ CheckAndRemovePromiseHook(env());
+
Local<Value> ret_v;
if (!ret.ToLocal(&ret_v)) {
return Local<Value>();
@@ -717,6 +713,8 @@ Local<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
CHECK_EQ(env()->current_async_id(), 0);
CHECK_EQ(env()->trigger_id(), 0);
+ CheckAndRemovePromiseHook(env());
+
Local<Object> process = env()->process_object();
if (tick_info->length() == 0) {
diff --git a/src/async-wrap.h b/src/async-wrap.h
index fa37ea1..d657343 100644
--- a/src/async-wrap.h
+++ b/src/async-wrap.h
@@ -111,6 +111,8 @@ class AsyncWrap : public BaseObject {
static bool EmitBefore(Environment* env, double id);
static bool EmitAfter(Environment* env, double id);
+ static void CheckAndRemovePromiseHook(Environment* env);
+
inline ProviderType provider_type() const;
inline double get_id() const;
diff --git a/src/env.cc b/src/env.cc
index 0087f71..1f4ed15 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -204,6 +204,7 @@ bool Environment::RemovePromiseHook(promise_hook_func fn, void* arg) {
if (it == promise_hooks_.end()) return false;
+ CHECK_GT(it->enable_count_, 0);
if (--it->enable_count_ > 0) return true;
promise_hooks_.erase(it);
diff --git a/src/node.cc b/src/node.cc
index bbce102..090bcab 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -1326,6 +1326,8 @@ MaybeLocal<Value> MakeCallback(Environment* env,
ret = callback->Call(env->context(), recv, argc, argv);
+ AsyncWrap::CheckAndRemovePromiseHook(env);
+
if (ret.IsEmpty()) {
// NOTE: For backwards compatibility with public API we return Undefined()
// if the top level call threw.
@@ -1365,7 +1367,11 @@ MaybeLocal<Value> MakeCallback(Environment* env,
return ret;
}
- if (env->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
+ auto tc_ret = env->tick_callback_function()->Call(process, 0, nullptr);
+
+ AsyncWrap::CheckAndRemovePromiseHook(env);
+
+ if (tc_ret.IsEmpty()) {
return Undefined(env->isolate());
}
@@ -3611,6 +3617,8 @@ void LoadEnvironment(Environment* env) {
// like Node's I/O bindings may want to replace 'f' with their own function.
Local<Value> arg = env->process_object();
f->Call(Null(env->isolate()), 1, &arg);
+
+ AsyncWrap::CheckAndRemovePromiseHook(env);
}
static void PrintHelp() { |
@trevnorris I see, that would probably work. But what’s the advantage? It should have the same effects, right? In that case I’d probably prefer a version that keeps the information flow a bit more localized, instead of spread out over the various async calling facilities… (Side note, since this is reminding me: What’s speaking against merging the two MakeCallback implementations? It seemed to pass testing last I checked, and the methods are already in that dangerous almost-but-not-quite identical shape…) |
It causes a timing discrepancy in one edge case. Since the microtask queue is only drained after the nexttick queue is drained there's a timing issue with 'use strict';
process.on('uncaughtException', () => { });
const s = setImmediate(() => Promise.resolve(42));
const h = require('async_hooks').createHook({
init(id, type) { process._rawDebug(id, type) },
}).enable();
process.nextTick(() => {
process.nextTick(() => h.disable());
throw new Error();
}); I'd expect no
To verify, you mean |
@trevnorris Ah, I see… I dug around that whole code a bit on the weekend, and yes, it’s a terrible tangle of edge cases. :/ Maybe
Yup, those two. :) |
Ah yes. Should have seen this before. This wraps all necessary locations.
If you can get
Let me look back at why they weren't merged. When I first implemented |
Nothing particularly exciting, I’d say: diff --git a/src/async-wrap.cc b/src/async-wrap.cc
index d7cdc4198c94..6ab8ec227537 100644
--- a/src/async-wrap.cc
+++ b/src/async-wrap.cc
@@ -666,65 +666,10 @@ void AsyncWrap::EmitAsyncInit(Environment* env,
Local<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
int argc,
Local<Value>* argv) {
- CHECK(env()->context() == env()->isolate()->GetCurrentContext());
-
- Environment::AsyncCallbackScope callback_scope(env());
-
- Environment::AsyncHooks::ExecScope exec_scope(env(),
- get_id(),
- get_trigger_id());
-
- if (!PreCallbackExecution(this, true)) {
- return Local<Value>();
- }
-
- // Finally... Get to running the user's callback.
- MaybeLocal<Value> ret = cb->Call(env()->context(), object(), argc, argv);
-
- Local<Value> ret_v;
- if (!ret.ToLocal(&ret_v)) {
- return Local<Value>();
- }
-
- if (!PostCallbackExecution(this, true)) {
- return Local<Value>();
- }
-
- exec_scope.Dispose();
-
- if (callback_scope.in_makecallback()) {
- return ret_v;
- }
-
- Environment::TickInfo* tick_info = env()->tick_info();
-
- if (tick_info->length() == 0) {
- 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(), 0);
- CHECK_EQ(env()->trigger_id(), 0);
-
- Local<Object> process = env()->process_object();
-
- if (tick_info->length() == 0) {
- tick_info->set_index(0);
- return ret_v;
- }
-
- MaybeLocal<Value> rcheck =
- env()->tick_callback_function()->Call(env()->context(),
- process,
- 0,
- nullptr);
-
- // Make sure the stack unwound properly.
- CHECK_EQ(env()->current_async_id(), 0);
- CHECK_EQ(env()->trigger_id(), 0);
-
- return rcheck.IsEmpty() ? Local<Value>() : ret_v;
+ return node::MakeCallback(env()->isolate(),
+ object(), cb, argc, argv,
+ get_id(), get_trigger_id())
+ .FromMaybe(Local<Value>());
}
diff --git a/src/node.cc b/src/node.cc
index bbce10220fe1..643e1096cac9 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -1355,8 +1355,8 @@ MaybeLocal<Value> MakeCallback(Environment* env,
// 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(), async_id);
- CHECK_EQ(env->trigger_id(), trigger_id);
+ /*CHECK_EQ(env->current_async_id(), async_id);
+ CHECK_EQ(env->trigger_id(), trigger_id);*/
Local<Object> process = env->process_object();
I don’t quite recall why the last checks were failing. |
9718b5f
to
fa167a1
Compare
Until now, the async_hooks PromiseHook did not register the Promise’s async id and trigger id on the id stack, so inside the `.then()` handler those ids would be invalid. To fix this, add push and pop calls to its `before` and `after` parts, respectively. Some care needs to be taken for the cases that the Promise hook is being disabled or enabled during the execution of a Promise handler; in the former case, actually removing the hook is delayed by adding another task to the microtask queue, in the latter case popping the id off the async id stack is skipped if the ids don’t match. Fixes: nodejs#13583 PR-URL: nodejs#13585 Reviewed-By: Trevor Norris <trevnorris@gmail.com>
fa167a1
to
af1a551
Compare
Landed in af1a551 |
Until now, the async_hooks PromiseHook did not register the Promise’s async id and trigger id on the id stack, so inside the `.then()` handler those ids would be invalid. To fix this, add push and pop calls to its `before` and `after` parts, respectively. Some care needs to be taken for the cases that the Promise hook is being disabled or enabled during the execution of a Promise handler; in the former case, actually removing the hook is delayed by adding another task to the microtask queue, in the latter case popping the id off the async id stack is skipped if the ids don’t match. Fixes: #13583 PR-URL: #13585 Reviewed-By: Trevor Norris <trevnorris@gmail.com>
Until now, the async_hooks PromiseHook did not register the Promise’s async id and trigger id on the id stack, so inside the `.then()` handler those ids would be invalid. To fix this, add push and pop calls to its `before` and `after` parts, respectively. Some care needs to be taken for the cases that the Promise hook is being disabled or enabled during the execution of a Promise handler; in the former case, actually removing the hook is delayed by adding another task to the microtask queue, in the latter case popping the id off the async id stack is skipped if the ids don’t match. Fixes: #13583 PR-URL: #13585 Reviewed-By: Trevor Norris <trevnorris@gmail.com>
Until now, the async_hooks PromiseHook did not register the Promise’s async id and trigger id on the id stack, so inside the `.then()` handler those ids would be invalid. To fix this, add push and pop calls to its `before` and `after` parts, respectively. Some care needs to be taken for the cases that the Promise hook is being disabled or enabled during the execution of a Promise handler; in the former case, actually removing the hook is delayed by adding another task to the microtask queue, in the latter case popping the id off the async id stack is skipped if the ids don’t match. Fixes: #13583 PR-URL: #13585 Reviewed-By: Trevor Norris <trevnorris@gmail.com>
Until now, the async_hooks PromiseHook did not register the Promise’s async id and trigger id on the id stack, so inside the `.then()` handler those ids would be invalid. To fix this, add push and pop calls to its `before` and `after` parts, respectively. Some care needs to be taken for the cases that the Promise hook is being disabled or enabled during the execution of a Promise handler; in the former case, actually removing the hook is delayed by adding another task to the microtask queue, in the latter case popping the id off the async id stack is skipped if the ids don’t match. Fixes: #13583 PR-URL: #13585 Reviewed-By: Trevor Norris <trevnorris@gmail.com>
Until now, the async_hooks PromiseHook did not register the Promise’s async id and trigger id on the id stack, so inside the `.then()` handler those ids would be invalid. To fix this, add push and pop calls to its `before` and `after` parts, respectively. Some care needs to be taken for the cases that the Promise hook is being disabled or enabled during the execution of a Promise handler; in the former case, actually removing the hook is delayed by adding another task to the microtask queue, in the latter case popping the id off the async id stack is skipped if the ids don’t match. Fixes: #13583 PR-URL: #13585 Reviewed-By: Trevor Norris <trevnorris@gmail.com>
Fixes: #13583
This won’t fix everything, but I think this is something that just qualifies as a right thing to do.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
async_hooks
/cc @nodejs/async_hooks