-
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: use parent promise as triggerId in init hook #13367
Conversation
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 is a good idea, but you asked about how to detect if a promise was created without a parent promise.
Can I say that triggerId is 1 means this Promise is just a newly created Promise without any parent chain?
Not always,
triggerId = 1
is just thetrigger_id
for the root, if you create a promise inside the callback of another asynchronous operation you will get anothertrigger_id
.If we implement as suggested, you can check if
triggerId === async_hooks.currentId()
. If that istrue
then the promise has no parent (there might be an edge case I haven't thought of).
I have thought some more about it, and the triggerId === async_hooks.currentId()
condition is not sufficient it is just necessary (sorry for the math language).
Consider the following code:
const p0 = new Promise((resolve) => resolve(1));
const p1 = p0.then(function () {
// async_hooks.currentId() === asyncId(p1)
p2 = p1.then(function () { // trigger_id = asyncId(p1)
});
return 2;
});
In this case async_hooks.currentId() == trigger_id
but p2
is a parent of p1
.
I still think setting the trigger_id
like this is useful, but it doesn't allow for detecting if there is a parent promise.
A possible solution could be have the resource
be an actual PromiseWrap
object and then store the meta information on that object. We have talked about this before.
src/async-wrap.cc
Outdated
// current promise's triggerId | ||
double trigger_id = Unwrap<PromiseWrap>(parent.As<Promise>())->get_id(); | ||
env->set_init_trigger_id(trigger_id); | ||
wrap = new PromiseWrap(env, promise, silent); |
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.
Maybe just
+ // set parent promise's async Id as this promise's triggerId
+ if (parent->IsPromise()) {
+ // parent promise exists, current promise
+ // is a chained promise, so we set parent promise's id as
+ // current promise's triggerId
+ double trigger_id = Unwrap<PromiseWrap>(parent.As<Promise>())->get_id();
+ env->set_init_trigger_id(trigger_id);
+ }
wrap = new PromiseWrap(env, promise, silent);
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.
@AndreasMadsen , Thank very much for the review, I have modified this one.
28d4db5
to
d721c6b
Compare
@AndreasMadsen , about the sample code you provided, Consider the following code: const p0 = new Promise((resolve) => resolve(1));
const p1 = p0.then(function () {
// async_hooks.currentId() === asyncId(p1)
p2 = p1.then(function () { // trigger_id = asyncId(p1)
});
return 2;
}); I make a test with this PR compiled node. const async_hooks = require('async_hooks');
function init(id, provider, parentId, parentHandle) {
process._rawDebug('init: ', id, 'triggerId: ', parentId, 'currentId', async_hooks.currentId());
}
function before(id) {
process._rawDebug('before', id, async_hooks.currentId());
}
function after(id) {
process._rawDebug('after', id, async_hooks.currentId());
}
function destroy(id) {
process._rawDebug('destroy', id);
}
async_hooks.createHook({init, before, after, destroy}).enable();
debugger;
const p = new Promise ( resolve => {
process._rawDebug('execute p');
resolve(1);} );
const p1 = p.then(val => {
process._rawDebug('p1 then', val);
const p2 = p1.then((val1) => {
process._rawDebug('p2 then', val1);
});
return 2; }); the output is
p is promise without parent, so I think this a correct result. And I tried another case setTimeout(() => {
const p = new Promise(resolve => {
process._rawDebug('execute p');
resolve(1);
});
const p1 = p.then(val => {
process._rawDebug('p1 then', val);
const p2 = p1.then((val1) => {
process._rawDebug('p2 then', val1);
});
return 2;
});
}, 100); the output is
I think the result is also ok, but I don't quite understand the why Please review. Thank you very much! |
Interesting, I think something is wrong with init: 2 triggerId: 1 currentId 1
execute p
init: 3 triggerId: 2 currentId 1
before 3 1
p1 then 1
init: 4 triggerId: 3 currentId 1 // wrong, currentId should be 3
after 3 1
before 4 1
p2 then 2
after 4 1 PS: I very much doubt this change is responsible for the bug. /cc @nodejs/async_hooks |
@AndreasMadsen, I am not sure I understand the lifecycle of currentId, so I do not know what is the correct result,just like the another sample involve setTimeout will have a currentId 0,I do not understand why. |
@JiaLiPassion The In some cases, there is no |
@AndreasMadsen , Thank you for the explanation.
Yeah, this is making sense.
you mean there is only And yes, in my setTimeout example, there is |
For example in the TCP server you will get:
This is because the TCP socket is created in C++ land and then send to the callback. Since this happens before any javascript the |
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.
The code change looks good to me, aside from one edge case. I still am not 100 % sure about this being the right thing, but if Andreas says so, I trust him.
I would still really like to see a test that shows that getting the original execution scope remains possible, because that is something that actual use cases will rely on.
src/async-wrap.cc
Outdated
// parent promise exists, current promise | ||
// is a chained promise, so we set parent promise's id as | ||
// current promise's triggerId | ||
double trigger_id = Unwrap<PromiseWrap>(parent.As<Promise>())->get_id(); |
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.
You need to also check whether Unwrap<PromiseWrap>()
returned nullptr
(that could happen if async_hooks
were enabled after parent
was created, but before promise
was created).
If it is, you’ll need to basically do the same thing we’re doing just below; something along the lines of:
auto parent_wrap = new PromiseWrap(env, parent, true);
parent_wrap->MakeWeak(parent_wrap);
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.
@addaleax , thank you for the review.
So the code should look like
auto parent_wrap = Unwrap<PromiseWrap>(parent.As<Promise>());
if (parent_wrap != nullptr) {
double trigger_id = parent_wrap->get_id();
env->set_init_trigger_id(trigger_id);
}
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.
@JiaLiPassion I think you would still want to create a new PromiseWrap
for the parent
Promise. If you do that, you don’t need to check if (parent_wrap != nullptr)
.
(Basically, look at the existing code here and how it adds a parent_wrap
when there isn’t any. If it’s unclear, feel free to ping me and I’ll explain in more detail. :))
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.
@addaleax , thank you so much! sorry for so many basic questions...
So the code should like
// create a new PromiseWrap for parent promise
auto parent_wrap = new PromiseWrap(env, parent, true);
parent_wrap->MakeWeak(parent_wrap);
// get id from parentWrap
double trigger_id = parent_wrap->get_id();
env->set_init_trigger_id(trigger_id);
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.
The new PromiseWrap
should only be created if Unwrap<>
gives you a nullptr
(otherwise there already is one, so we don’t need that). But yes, that’s the general idea.
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.
@addaleax , thank you, so it will look like
if (parent->IsPromise()) {
auto parent_wrap = Unwrap<PromiseWrap>(parent.As<Promise>());
if (parent_wrap == nullptr) {
// create a new PromiseWrap for parent promise with silent parameter
parent_wrap = new PromiseWrap(env, parent, true);
parent_wrap->MakeWeak(parent_wrap);
}
// get id from parentWrap
double trigger_id = parent_wrap->get_id();
env->set_init_trigger_id(trigger_id);
}
}
I just updated the code, ping @addaleax !
Sorry to burst your bubble, but the Also since this doesn't solve the problem that @JiaLiPassion originally asked, regarding detecting if a parentPromise exists, I would like to explore some alternatives. tl;dr; I think this makes sense, but I'm generally uncertain about
Yeah, we need to fix the |
@AndreasMadsen Regarding
Writing this comment, it still seems to me like we should maybe just expose the parent Promises async id as an extra argument to the |
@addaleax It is the first one, but there is a thin line between causality and parent resource, which is what causes my confusion. From the documentation:
|
So if expose the parent Promise async id as an extra argument, it can resolve my original issue, that if the argument exists, the the promise is a chained one, is that correct?
do you mean the case like this? async function test() {
await new Promise(resolve => resolve(1));
}
process._rawDebug('before', async_hooks.currentId());
test();
process._rawDebug('after', async_hooks.currentId()); |
@AndreasMadsen Okay, makes sense.
@JiaLiPassion Yes, I’d say so.
More like this: async function test() {
process._rawDebug('before', async_hooks.currentId());
await new Promise(resolve => resolve(1));
process._rawDebug('after', async_hooks.currentId());
} |
I think extra arguments to |
That works for me; but it means that we’d have to switch the |
I think that would be the best. It is also what we discussed in the original Promise PR and what @Fishrock123 advocated for. |
@AndreasMadsen , sorry I just lost...
Do you mean use |
Sort of, but we definitely shouldn't go through JS for that. For the purpose of a resource = {
promise // reference to promise,
parentPromise // reference to parentPromise or undefined
} |
@AndreasMadsen , can I just change the how to create a new Local<Object> promise_resource = Object::New(env->isolate());
FORCE_SET_TARGET_FIELD(promise_resource, "promise", promise);
FORCE_SET_TARGET_FIELD(promise_resource, "parentPromise", parent_promise);
wrap = new PromiseWrap(env, promise_resource, silent);
|
@JiaLiPassion I don't think that is enough. This is as far as my V8-C++ knowledge goes, I'm not sure how to tie in the GC of |
@hayes Thanks for the explanation. I misunderstood what you meant by Plese create an issue, but don't do it around the idea the
I'm pretty sure it does. But we can discuss that in the issue. |
Please keep this issue on track, this PR is just about fixing
|
@AndreasMadsen , I have updated the test cases and assert message. please review. |
@JiaLiPassion Yes, that is how you’d check whether a parent promise exists. (Now that you mention it: Maybe |
@addaleax , got it , thank you very much!!! Would you please make another PR to implement it? |
@JiaLiPassion I’ll wait until you land this PR, then I can do that. :) |
const initHooks = require('./init-hooks'); | ||
const { checkInvocations } = require('./hook-checks'); | ||
|
||
const p = (new Promise(common.mustCall(executor))); |
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.
You don’t need the outer parentheses here
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.
yes, I will remove it.
function afterresolution(val) { | ||
assert.strictEqual(val, 5); | ||
return val; | ||
} |
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 would be a bit more readable if you just inlined executor
and afterresolution
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.
yes, you are right, I will change it.
} | ||
|
||
process.on('exit', onexit); | ||
function onexit() { |
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.
same for onexit
, using process.on('exit', () => {
works just fine
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.
yes, I will change it.
test/async-hooks/test-promise.js
Outdated
@@ -14,7 +14,7 @@ p.then(afterresolution); | |||
|
|||
function executor(resolve, reject) { | |||
const as = hooks.activitiesOfTypes('PROMISE'); | |||
assert.strictEqual(as.length, 1, 'one activities'); | |||
assert.strictEqual(as.length, 1, 'one activity'); |
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.
If you want to avoid merge conflicts with #13243, you can just drop this change
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.
@addaleax , ok , so I just revert it.
test/async-hooks/test-promise.js
Outdated
@@ -46,7 +46,8 @@ function onexit() { | |||
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'); | |||
assert.strictEqual(a1.triggerId, a0.uid, | |||
'child promise should use parent uid as 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.
Yeah, I would just not use an error message here (i.e. assert.strictEqual(a1.triggerId, a0.uid);
). Sorry the tests are written this way.
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.
ok, so I just modify it to
assert.strictEqual(a1.triggerId, a0.uid);
Left a few style nits. |
I will fix these, there are some other changes I would like. |
@AndreasMadsen , ok, thank you! so I don't need to update the source which were reviewed by @addaleax ? |
ce12ab1
to
b8f8f4b
Compare
@JiaLiPassion yep, I fixed it for you :) I didn't really understand why the |
@AndreasMadsen , thank you very much!!! you and @addaleax give me great help!!! |
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. async_hooks
it a beast, so I'm happy to see that made it. I look forward to future interactions with zone.js
.
Landed in 8741e3c 🎉 @JiaLiPassion I believe this is your first commit to core, if so, welcome on board! Thank you for the work you have been putting into this, and I think I speak for everyone in this thread if I say that we’d love to see more of that. If you’re interested, we can probably add you to the @nodejs/async_hooks review team that gets @mentioned on issues and PRs related to @jasnell I’ve left you off as a reviewer because the PR has undergone quite some change since you reviewed, and wasn’t really ready at that point anyway. I hope that’s okay. |
async_hooks init callback will be triggered when promise newly created, in previous version, the parent promise which pass from chrome V8 PromiseHook is ignored, so we can't tell the promise is a pure new promise or a chained promise. In this commit, we use the parent promise's id as triggerId to trigger the init callback. Fixes: #13302 PR-URL: #13367 Reviewed-By: Andreas Madsen <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
@addaleax , Thank you so much for all the great help, I am so glad to join here and definitely want to do more in the future.
Yes!!! I am very happy to join here and it will be my honor to join @nodejs/async_hooks review team to do more contributions and want to learn more from here. Thank you and @AndreasMadsen again for all the great help!!! |
async_hooks init callback will be triggered when promise newly created, in previous version, the parent promise which pass from chrome V8 PromiseHook is ignored, so we can't tell the promise is a pure new promise or a chained promise. In this commit, we use the parent promise's id as triggerId to trigger the init callback. Fixes: #13302 PR-URL: #13367 Reviewed-By: Andreas Madsen <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
* **Async Hooks** * When one `Promise` leads to the creation of a new `Promise`, the parent `Promise` will be identified as the trigger [[`135f4e6643`](nodejs@135f4e6643)] [nodejs#13367](nodejs#13367). * **Dependencies** * libuv has been updated to 1.12.0 [[`968596ec77`](nodejs@968596ec77)] [nodejs#13306](nodejs#13306). * npm has been updated to 5.0.3 [[`ffa7debd7a`](nodejs@ffa7debd7a)] [nodejs#13487](nodejs#13487). * **File system** * The `fs.exists()` function now works correctly with `util.promisify()` [[`6e0eccd7a1`](nodejs@6e0eccd7a1)] [nodejs#13316](nodejs#13316). * fs.Stats times are now also available as numbers [[`c756efb25a`](nodejs@c756efb25a)] [nodejs#13173](nodejs#13173). * **Inspector** * It is now possible to bind to a random port using `--inspect=0` [[`cc6ec2fb27`](nodejs@cc6ec2fb27)] [nodejs#5025](nodejs#5025). * **Zlib** * A regression in the Zlib module that made it impossible to properly subclasses `zlib.Deflate` and other Zlib classes has been fixed. [[`6aeb555cc4`](nodejs@6aeb555cc4)] [nodejs#13374](nodejs#13374).
* **Async Hooks** * When one `Promise` leads to the creation of a new `Promise`, the parent `Promise` will be identified as the trigger [[`135f4e6643`](135f4e6643)] [#13367](#13367). * **Dependencies** * libuv has been updated to 1.12.0 [[`968596ec77`](968596ec77)] [#13306](#13306). * npm has been updated to 5.0.3 [[`ffa7debd7a`](ffa7debd7a)] [#13487](#13487). * **File system** * The `fs.exists()` function now works correctly with `util.promisify()` [[`6e0eccd7a1`](6e0eccd7a1)] [#13316](#13316). * fs.Stats times are now also available as numbers [[`c756efb25a`](c756efb25a)] [#13173](#13173). * **Inspector** * It is now possible to bind to a random port using `--inspect=0` [[`cc6ec2fb27`](cc6ec2fb27)] [#5025](#5025). * **Zlib** * A regression in the Zlib module that made it impossible to properly subclasses `zlib.Deflate` and other Zlib classes has been fixed. [[`6aeb555cc4`](6aeb555cc4)] [#13374](#13374).
* **Async Hooks** * When one `Promise` leads to the creation of a new `Promise`, the parent `Promise` will be identified as the trigger [[`135f4e6643`](135f4e6643)] [#13367](#13367). * **Dependencies** * libuv has been updated to 1.12.0 [[`968596ec77`](968596ec77)] [#13306](#13306). * npm has been updated to 5.0.3 [[`ffa7debd7a`](ffa7debd7a)] [#13487](#13487). * **File system** * The `fs.exists()` function now works correctly with `util.promisify()` [[`6e0eccd7a1`](6e0eccd7a1)] [#13316](#13316). * fs.Stats times are now also available as numbers [[`c756efb25a`](c756efb25a)] [#13173](#13173). * **Inspector** * It is now possible to bind to a random port using `--inspect=0` [[`cc6ec2fb27`](cc6ec2fb27)] [#5025](#5025). * **Zlib** * A regression in the Zlib module that made it impossible to properly subclasses `zlib.Deflate` and other Zlib classes has been fixed. [[`6aeb555cc4`](6aeb555cc4)] [#13374](#13374).
async_hooks init callback will be triggered when promise newly created,
in previous version, the parent promise which pass from chrome V8 PromiseHook
is ignored, so we can't tell the promise is a pure new promise or a
chained promise.
In this commit, we use the parent promise's id as triggerId to
trigger the init callback.
Fixes: #13302
Thanks for @AndreasMadsen and @addaleax 's great help!
Please review.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)