Skip to content

Commit 2122e2f

Browse files
trevnorrisaddaleax
authored andcommitted
async_wrap: use kTotals to enable PromiseHook
Keep a total of enabled hook callbacks in kTotals. This value is used to track whether node::PromiseHook (src/async-wrap.cc) should be enabled or disabled. Don't enable node::PromiseHook, using enablePromiseHook(), until a hook has been added. Then, using disablePromiseHook(), disable node::PromiseHook when all hooks have been disabled. Need to use a native test in order to check the internal field of the Promise and check for a PromiseWrap. PR-URL: #13509 Reviewed-By: Andreas Madsen <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
1 parent 96279e8 commit 2122e2f

File tree

7 files changed

+177
-12
lines changed

7 files changed

+177
-12
lines changed

lib/async_hooks.js

+27-11
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ var tmp_async_hook_fields = null;
3838
// Each constant tracks how many callbacks there are for any given step of
3939
// async execution. These are tracked so if the user didn't include callbacks
4040
// for a given step, that step can bail out early.
41-
const { kInit, kBefore, kAfter, kDestroy, kCurrentAsyncId, kCurrentTriggerId,
42-
kAsyncUidCntr, kInitTriggerId } = async_wrap.constants;
41+
const { kInit, kBefore, kAfter, kDestroy, kTotals, kCurrentAsyncId,
42+
kCurrentTriggerId, kAsyncUidCntr, kInitTriggerId } = async_wrap.constants;
4343

4444
const { async_id_symbol, trigger_id_symbol } = async_wrap;
4545

@@ -50,7 +50,9 @@ const after_symbol = Symbol('after');
5050
const destroy_symbol = Symbol('destroy');
5151

5252
// Setup the callbacks that node::AsyncWrap will call when there are hooks to
53-
// process. They use the same functions as the JS embedder API.
53+
// process. They use the same functions as the JS embedder API. These callbacks
54+
// are setup immediately to prevent async_wrap.setupHooks() from being hijacked
55+
// and the cost of doing so is negligible.
5456
async_wrap.setupHooks({ init,
5557
before: emitBeforeN,
5658
after: emitAfterN,
@@ -103,14 +105,21 @@ class AsyncHook {
103105
if (hooks_array.includes(this))
104106
return this;
105107

108+
const prev_kTotals = hook_fields[kTotals];
109+
hook_fields[kTotals] = 0;
110+
106111
// createHook() has already enforced that the callbacks are all functions,
107112
// so here simply increment the count of whether each callbacks exists or
108113
// not.
109-
hook_fields[kInit] += +!!this[init_symbol];
110-
hook_fields[kBefore] += +!!this[before_symbol];
111-
hook_fields[kAfter] += +!!this[after_symbol];
112-
hook_fields[kDestroy] += +!!this[destroy_symbol];
114+
hook_fields[kTotals] += hook_fields[kInit] += +!!this[init_symbol];
115+
hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
116+
hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
117+
hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
113118
hooks_array.push(this);
119+
120+
if (prev_kTotals === 0 && hook_fields[kTotals] > 0)
121+
async_wrap.enablePromiseHook();
122+
114123
return this;
115124
}
116125

@@ -121,11 +130,18 @@ class AsyncHook {
121130
if (index === -1)
122131
return this;
123132

124-
hook_fields[kInit] -= +!!this[init_symbol];
125-
hook_fields[kBefore] -= +!!this[before_symbol];
126-
hook_fields[kAfter] -= +!!this[after_symbol];
127-
hook_fields[kDestroy] -= +!!this[destroy_symbol];
133+
const prev_kTotals = hook_fields[kTotals];
134+
hook_fields[kTotals] = 0;
135+
136+
hook_fields[kTotals] += hook_fields[kInit] -= +!!this[init_symbol];
137+
hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol];
138+
hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol];
139+
hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
128140
hooks_array.splice(index, 1);
141+
142+
if (prev_kTotals > 0 && hook_fields[kTotals] === 0)
143+
async_wrap.disablePromiseHook();
144+
129145
return this;
130146
}
131147
}

src/async-wrap.cc

+7-1
Original file line numberDiff line numberDiff line change
@@ -335,12 +335,17 @@ static void PromiseHook(PromiseHookType type, Local<Promise> promise,
335335
Local<Value> parent, void* arg) {
336336
Local<Context> context = promise->CreationContext();
337337
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+
338342
Local<Value> resource_object_value = promise->GetInternalField(0);
339343
PromiseWrap* wrap = nullptr;
340344
if (resource_object_value->IsObject()) {
341345
Local<Object> resource_object = resource_object_value.As<Object>();
342346
wrap = Unwrap<PromiseWrap>(resource_object);
343347
}
348+
344349
if (type == PromiseHookType::kInit || wrap == nullptr) {
345350
bool silent = type != PromiseHookType::kInit;
346351
PromiseWrap* parent_wrap = nullptr;
@@ -368,6 +373,7 @@ static void PromiseHook(PromiseHookType type, Local<Promise> promise,
368373
} else if (type == PromiseHookType::kResolve) {
369374
// TODO(matthewloring): need to expose this through the async hooks api.
370375
}
376+
371377
CHECK_NE(wrap, nullptr);
372378
if (type == PromiseHookType::kBefore) {
373379
PreCallbackExecution(wrap, false);
@@ -401,7 +407,6 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
401407
SET_HOOK_FN(before);
402408
SET_HOOK_FN(after);
403409
SET_HOOK_FN(destroy);
404-
env->AddPromiseHook(PromiseHook, nullptr);
405410
#undef SET_HOOK_FN
406411

407412
{
@@ -542,6 +547,7 @@ void AsyncWrap::Initialize(Local<Object> target,
542547
SET_HOOKS_CONSTANT(kBefore);
543548
SET_HOOKS_CONSTANT(kAfter);
544549
SET_HOOKS_CONSTANT(kDestroy);
550+
SET_HOOKS_CONSTANT(kTotals);
545551
SET_HOOKS_CONSTANT(kCurrentAsyncId);
546552
SET_HOOKS_CONSTANT(kCurrentTriggerId);
547553
SET_HOOKS_CONSTANT(kAsyncUidCntr);

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ class Environment {
344344
kBefore,
345345
kAfter,
346346
kDestroy,
347+
kTotals,
347348
kFieldsCount,
348349
};
349350

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#include <node.h>
2+
#include <v8.h>
3+
4+
namespace {
5+
6+
using v8::FunctionCallbackInfo;
7+
using v8::Isolate;
8+
using v8::Local;
9+
using v8::NewStringType;
10+
using v8::Object;
11+
using v8::Promise;
12+
using v8::String;
13+
using v8::Value;
14+
15+
static void ThrowError(Isolate* isolate, const char* err_msg) {
16+
Local<String> str = String::NewFromOneByte(
17+
isolate,
18+
reinterpret_cast<const uint8_t*>(err_msg),
19+
NewStringType::kNormal).ToLocalChecked();
20+
isolate->ThrowException(str);
21+
}
22+
23+
static void GetPromiseField(const FunctionCallbackInfo<Value>& args) {
24+
auto isolate = args.GetIsolate();
25+
26+
if (!args[0]->IsPromise())
27+
return ThrowError(isolate, "arg is not an Promise");
28+
29+
auto p = args[0].As<Promise>();
30+
if (p->InternalFieldCount() < 1)
31+
return ThrowError(isolate, "Promise has no internal field");
32+
33+
auto l = p->GetInternalField(0);
34+
args.GetReturnValue().Set(l);
35+
}
36+
37+
inline void Initialize(v8::Local<v8::Object> binding) {
38+
NODE_SET_METHOD(binding, "getPromiseField", GetPromiseField);
39+
}
40+
41+
NODE_MODULE(binding, Initialize)
42+
43+
} // anonymous namespace
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
6+
'sources': [ 'binding.cc' ]
7+
}
8+
]
9+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const assert = require('assert');
5+
const async_hooks = require('async_hooks');
6+
const binding = require(`./build/${common.buildType}/binding`);
7+
8+
// Baseline to make sure the internal field isn't being set.
9+
assert.strictEqual(
10+
binding.getPromiseField(Promise.resolve(1)),
11+
0,
12+
'Promise internal field used despite missing enabled AsyncHook');
13+
14+
const hook0 = async_hooks.createHook({}).enable();
15+
16+
// Check that no PromiseWrap is created when there are no hook callbacks.
17+
assert.strictEqual(
18+
binding.getPromiseField(Promise.resolve(1)),
19+
0,
20+
'Promise internal field used despite missing enabled AsyncHook');
21+
22+
hook0.disable();
23+
24+
let pwrap = null;
25+
const hook1 = async_hooks.createHook({
26+
init(id, type, tid, resource) {
27+
pwrap = resource;
28+
}
29+
}).enable();
30+
31+
// Check that the internal field returns the same PromiseWrap passed to init().
32+
assert.strictEqual(
33+
binding.getPromiseField(Promise.resolve(1)),
34+
pwrap,
35+
'Unexpected PromiseWrap');
36+
37+
hook1.disable();
38+
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');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const async_hooks = require('async_hooks');
6+
const EXPECTED_INITS = 2;
7+
let p_resource = null;
8+
let p_er = null;
9+
let p_inits = 0;
10+
11+
// Not useful to place common.mustCall() around 'exit' event b/c it won't be
12+
// able to check it anway.
13+
process.on('exit', (code) => {
14+
if (code !== 0)
15+
return;
16+
if (p_er !== null)
17+
throw p_er;
18+
// Expecint exactly 2 PROMISE types to reach init.
19+
assert.strictEqual(p_inits, EXPECTED_INITS);
20+
});
21+
22+
const mustCallInit = common.mustCall(function init(id, type, tid, resource) {
23+
if (type !== 'PROMISE')
24+
return;
25+
p_inits++;
26+
p_resource = resource.promise;
27+
}, EXPECTED_INITS);
28+
29+
const hook = async_hooks.createHook({
30+
init: mustCallInit
31+
// Enable then disable to test whether disable() actually works.
32+
}).enable().disable().disable();
33+
34+
new Promise(common.mustCall((res) => {
35+
res(42);
36+
})).then(common.mustCall((val) => {
37+
hook.enable().enable();
38+
const p = new Promise((res) => res(val));
39+
assert.strictEqual(p, p_resource);
40+
hook.disable();
41+
return p;
42+
})).then(common.mustCall((val2) => {
43+
hook.enable();
44+
const p = new Promise((res) => res(val2));
45+
hook.disable();
46+
return p;
47+
})).catch((er) => p_er = er);

0 commit comments

Comments
 (0)