Skip to content

Commit 8f37f5d

Browse files
committed
async_hooks: proper id stacking for Promises
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 <[email protected]>
1 parent a45792a commit 8f37f5d

7 files changed

+97
-15
lines changed

src/async-wrap.cc

+18-8
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,7 @@ void PromiseWrap::GetParentId(Local<String> property,
333333

334334
static void PromiseHook(PromiseHookType type, Local<Promise> promise,
335335
Local<Value> parent, void* arg) {
336-
Local<Context> context = promise->CreationContext();
337-
Environment* env = Environment::GetCurrent(context);
338-
339-
// PromiseHook() should never be called if no hooks have been enabled.
340-
CHECK_GT(env->async_hooks()->fields()[AsyncHooks::kTotals], 0);
341-
336+
Environment* env = static_cast<Environment*>(arg);
342337
Local<Value> resource_object_value = promise->GetInternalField(0);
343338
PromiseWrap* wrap = nullptr;
344339
if (resource_object_value->IsObject()) {
@@ -376,9 +371,18 @@ static void PromiseHook(PromiseHookType type, Local<Promise> promise,
376371

377372
CHECK_NE(wrap, nullptr);
378373
if (type == PromiseHookType::kBefore) {
374+
env->async_hooks()->push_ids(wrap->get_id(), wrap->get_trigger_id());
379375
PreCallbackExecution(wrap, false);
380376
} else if (type == PromiseHookType::kAfter) {
381377
PostCallbackExecution(wrap, false);
378+
if (env->current_async_id() == wrap->get_id()) {
379+
// This condition might not be true if async_hooks was enabled during
380+
// the promise callback execution.
381+
// Popping it off the stack can be skipped in that case, because is is
382+
// known that it would correspond to exactly one call with
383+
// PromiseHookType::kBefore that was not witnessed by the PromiseHook.
384+
env->async_hooks()->pop_ids(wrap->get_id());
385+
}
382386
}
383387
}
384388

@@ -429,13 +433,19 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
429433

430434
static void EnablePromiseHook(const FunctionCallbackInfo<Value>& args) {
431435
Environment* env = Environment::GetCurrent(args);
432-
env->AddPromiseHook(PromiseHook, nullptr);
436+
env->AddPromiseHook(PromiseHook, static_cast<void*>(env));
433437
}
434438

435439

436440
static void DisablePromiseHook(const FunctionCallbackInfo<Value>& args) {
437441
Environment* env = Environment::GetCurrent(args);
438-
env->RemovePromiseHook(PromiseHook, nullptr);
442+
443+
// Delay the call to `RemovePromiseHook` because we might currently be
444+
// between the `before` and `after` calls of a Promise.
445+
env->isolate()->EnqueueMicrotask([](void* data) {
446+
Environment* env = static_cast<Environment*>(data);
447+
env->RemovePromiseHook(PromiseHook, data);
448+
}, static_cast<void*>(env));
439449
}
440450

441451

src/env.cc

+7-2
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,11 @@ void Environment::AddPromiseHook(promise_hook_func fn, void* arg) {
184184
[&](const PromiseHookCallback& hook) {
185185
return hook.cb_ == fn && hook.arg_ == arg;
186186
});
187-
CHECK_EQ(it, promise_hooks_.end());
188-
promise_hooks_.push_back(PromiseHookCallback{fn, arg});
187+
if (it != promise_hooks_.end()) {
188+
it->enable_count_++;
189+
return;
190+
}
191+
promise_hooks_.push_back(PromiseHookCallback{fn, arg, 1});
189192

190193
if (promise_hooks_.size() == 1) {
191194
isolate_->SetPromiseHook(EnvPromiseHook);
@@ -201,6 +204,8 @@ bool Environment::RemovePromiseHook(promise_hook_func fn, void* arg) {
201204

202205
if (it == promise_hooks_.end()) return false;
203206

207+
if (--it->enable_count_ > 0) return true;
208+
204209
promise_hooks_.erase(it);
205210
if (promise_hooks_.empty()) {
206211
isolate_->SetPromiseHook(nullptr);

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,7 @@ class Environment {
709709
struct PromiseHookCallback {
710710
promise_hook_func cb_;
711711
void* arg_;
712+
size_t enable_count_;
712713
};
713714
std::vector<PromiseHookCallback> promise_hooks_;
714715

test/addons/async-hooks-promise/test.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ assert.strictEqual(
3636

3737
hook1.disable();
3838

39-
// Check that internal fields are no longer being set.
40-
assert.strictEqual(
41-
binding.getPromiseField(Promise.resolve(1)),
42-
0,
43-
'Promise internal field used despite missing enabled AsyncHook');
39+
// Check that internal fields are no longer being set. This needs to be delayed
40+
// a bit because the `disable()` call only schedules disabling the hook in a
41+
// future microtask.
42+
setImmediate(() => {
43+
assert.strictEqual(
44+
binding.getPromiseField(Promise.resolve(1)),
45+
0,
46+
'Promise internal field used despite missing enabled AsyncHook');
47+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
const common = require('../common');
3+
const async_hooks = require('async_hooks');
4+
5+
const hook = async_hooks.createHook({
6+
init: common.mustCall(2),
7+
before: common.mustCall(1),
8+
after: common.mustNotCall()
9+
}).enable();
10+
11+
Promise.resolve(1).then(common.mustCall(() => {
12+
hook.disable();
13+
14+
Promise.resolve(42).then(common.mustCall());
15+
16+
process.nextTick(common.mustCall());
17+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
const common = require('../common');
3+
const async_hooks = require('async_hooks');
4+
5+
Promise.resolve(1).then(common.mustCall(() => {
6+
async_hooks.createHook({
7+
init: common.mustCall(),
8+
before: common.mustCall(),
9+
after: common.mustCall(2)
10+
}).enable();
11+
12+
process.nextTick(common.mustCall());
13+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const async_hooks = require('async_hooks');
5+
6+
common.crashOnUnhandledRejection();
7+
8+
const promiseAsyncIds = [];
9+
10+
async_hooks.createHook({
11+
init: common.mustCallAtLeast((id, type, triggerId) => {
12+
if (type === 'PROMISE') {
13+
// Check that the last known Promise is triggering the creation of
14+
// this one.
15+
assert.strictEqual(promiseAsyncIds[promiseAsyncIds.length - 1] || 1,
16+
triggerId);
17+
promiseAsyncIds.push(id);
18+
}
19+
}, 3),
20+
before: common.mustCall((id) => {
21+
assert.strictEqual(id, promiseAsyncIds[1]);
22+
}),
23+
after: common.mustCall((id) => {
24+
assert.strictEqual(id, promiseAsyncIds[1]);
25+
})
26+
}).enable();
27+
28+
Promise.resolve(42).then(common.mustCall(() => {
29+
assert.strictEqual(async_hooks.executionAsyncId(), promiseAsyncIds[1]);
30+
assert.strictEqual(async_hooks.triggerAsyncId(), promiseAsyncIds[0]);
31+
Promise.resolve(10);
32+
}));

0 commit comments

Comments
 (0)