-
Notifications
You must be signed in to change notification settings - Fork 30.5k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -333,12 +333,7 @@ void PromiseWrap::GetParentId(Local<String> property, | |
|
||
static void PromiseHook(PromiseHookType type, Local<Promise> promise, | ||
Local<Value> parent, void* arg) { | ||
Local<Context> context = promise->CreationContext(); | ||
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); | ||
|
||
Environment* env = static_cast<Environment*>(arg); | ||
Local<Value> resource_object_value = promise->GetInternalField(0); | ||
PromiseWrap* wrap = nullptr; | ||
if (resource_object_value->IsObject()) { | ||
|
@@ -376,9 +371,18 @@ static void PromiseHook(PromiseHookType type, Local<Promise> promise, | |
|
||
CHECK_NE(wrap, nullptr); | ||
if (type == PromiseHookType::kBefore) { | ||
env->async_hooks()->push_ids(wrap->get_id(), wrap->get_trigger_id()); | ||
PreCallbackExecution(wrap, false); | ||
} else if (type == PromiseHookType::kAfter) { | ||
PostCallbackExecution(wrap, false); | ||
if (env->current_async_id() == wrap->get_id()) { | ||
// This condition might not be true if async_hooks was enabled during | ||
// the promise callback execution. | ||
// Popping it off the stack can be skipped in that case, because is is | ||
// known that it would correspond to exactly one call with | ||
// PromiseHookType::kBefore that was not witnessed by the PromiseHook. | ||
env->async_hooks()->pop_ids(wrap->get_id()); | ||
} | ||
} | ||
} | ||
|
||
|
@@ -429,13 +433,19 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) { | |
|
||
static void EnablePromiseHook(const FunctionCallbackInfo<Value>& args) { | ||
Environment* env = Environment::GetCurrent(args); | ||
env->AddPromiseHook(PromiseHook, nullptr); | ||
env->AddPromiseHook(PromiseHook, static_cast<void*>(env)); | ||
} | ||
|
||
|
||
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 commentThe 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 |
||
// 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)); | ||
} | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,8 +36,12 @@ assert.strictEqual( | |
|
||
hook1.disable(); | ||
|
||
// Check that internal fields are no longer being set. | ||
assert.strictEqual( | ||
binding.getPromiseField(Promise.resolve(1)), | ||
0, | ||
'Promise internal field used despite missing enabled AsyncHook'); | ||
// 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. We can’t because we need the This shouldn’t be a problem, it’s just emitting events for nobody to witness. |
||
assert.strictEqual( | ||
binding.getPromiseField(Promise.resolve(1)), | ||
0, | ||
'Promise internal field used despite missing enabled AsyncHook'); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
'use strict'; | ||
const common = require('../common'); | ||
const async_hooks = require('async_hooks'); | ||
|
||
const hook = async_hooks.createHook({ | ||
init: common.mustCall(2), | ||
before: common.mustCall(1), | ||
after: common.mustNotCall() | ||
}).enable(); | ||
|
||
Promise.resolve(1).then(common.mustCall(() => { | ||
hook.disable(); | ||
|
||
Promise.resolve(42).then(common.mustCall()); | ||
|
||
process.nextTick(common.mustCall()); | ||
})); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
'use strict'; | ||
const common = require('../common'); | ||
const async_hooks = require('async_hooks'); | ||
|
||
Promise.resolve(1).then(common.mustCall(() => { | ||
async_hooks.createHook({ | ||
init: common.mustCall(), | ||
before: common.mustCall(), | ||
after: common.mustCall(2) | ||
}).enable(); | ||
|
||
process.nextTick(common.mustCall()); | ||
})); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
'use strict'; | ||
const common = require('../common'); | ||
const assert = require('assert'); | ||
const async_hooks = require('async_hooks'); | ||
|
||
common.crashOnUnhandledRejection(); | ||
|
||
const promiseAsyncIds = []; | ||
|
||
async_hooks.createHook({ | ||
init: common.mustCallAtLeast((id, type, triggerId) => { | ||
if (type === 'PROMISE') { | ||
// Check that the last known Promise is triggering the creation of | ||
// this one. | ||
assert.strictEqual(promiseAsyncIds[promiseAsyncIds.length - 1] || 1, | ||
triggerId); | ||
promiseAsyncIds.push(id); | ||
} | ||
}, 3), | ||
before: common.mustCall((id) => { | ||
assert.strictEqual(id, promiseAsyncIds[1]); | ||
}), | ||
after: common.mustCall((id) => { | ||
assert.strictEqual(id, promiseAsyncIds[1]); | ||
}) | ||
}).enable(); | ||
|
||
Promise.resolve(42).then(common.mustCall(() => { | ||
assert.strictEqual(async_hooks.executionAsyncId(), promiseAsyncIds[1]); | ||
assert.strictEqual(async_hooks.triggerAsyncId(), promiseAsyncIds[0]); | ||
Promise.resolve(10); | ||
})); |
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.