Skip to content

Commit 7e3a3c9

Browse files
trevnorrisaddaleax
authored andcommitted
async_hooks: initial async_hooks implementation
Fill this commit messsage with more details about the change once all changes are rebased. * Add lib/async_hooks.js * Add JS methods to AsyncWrap for handling the async id stack * Introduce AsyncReset() so that JS functions can reset the id and again trigger the init hooks, allow AsyncWrap::Reset() to be called from JS via asyncReset(). * Add env variable to test additional things in test/common.js PR-URL: #12892 Ref: #11883 Ref: #8531 Reviewed-By: Andreas Madsen <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Sam Roberts <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]>
1 parent c0bde73 commit 7e3a3c9

10 files changed

+699
-51
lines changed

lib/async_hooks.js

+488
Large diffs are not rendered by default.

lib/internal/module.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ function stripShebang(content) {
7777
}
7878

7979
const builtinLibs = [
80-
'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns',
81-
'domain', 'events', 'fs', 'http', 'https', 'net', 'os', 'path', 'punycode',
82-
'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'tls', 'tty',
83-
'url', 'util', 'v8', 'vm', 'zlib'
80+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'crypto',
81+
'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net', 'os',
82+
'path', 'punycode', 'querystring', 'readline', 'repl', 'stream',
83+
'string_decoder', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib'
8484
];
8585

8686
function addBuiltinLibsToObject(object) {

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
'node_core_target_name%': 'node',
2323
'library_files': [
2424
'lib/internal/bootstrap_node.js',
25+
'lib/async_hooks.js',
2526
'lib/assert.js',
2627
'lib/buffer.js',
2728
'lib/child_process.js',

src/async-wrap.cc

+84-40
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,48 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local<Value> wrapper) {
134134
// end RetainedAsyncInfo
135135

136136

137+
static void DestroyIdsCb(uv_idle_t* handle) {
138+
uv_idle_stop(handle);
139+
140+
Environment* env = Environment::from_destroy_ids_idle_handle(handle);
141+
142+
HandleScope handle_scope(env->isolate());
143+
Context::Scope context_scope(env->context());
144+
Local<Function> fn = env->async_hooks_destroy_function();
145+
146+
TryCatch try_catch(env->isolate());
147+
148+
std::vector<double> destroy_ids_list;
149+
destroy_ids_list.swap(*env->destroy_ids_list());
150+
for (auto current_id : destroy_ids_list) {
151+
// Want each callback to be cleaned up after itself, instead of cleaning
152+
// them all up after the while() loop completes.
153+
HandleScope scope(env->isolate());
154+
Local<Value> argv = Number::New(env->isolate(), current_id);
155+
MaybeLocal<Value> ret = fn->Call(
156+
env->context(), Undefined(env->isolate()), 1, &argv);
157+
158+
if (ret.IsEmpty()) {
159+
ClearFatalExceptionHandlers(env);
160+
FatalException(env->isolate(), try_catch);
161+
}
162+
}
163+
164+
env->destroy_ids_list()->clear();
165+
}
166+
167+
168+
static void PushBackDestroyId(Environment* env, double id) {
169+
if (env->async_hooks()->fields()[AsyncHooks::kDestroy] == 0)
170+
return;
171+
172+
if (env->destroy_ids_list()->empty())
173+
uv_idle_start(env->destroy_ids_idle_handle(), DestroyIdsCb);
174+
175+
env->destroy_ids_list()->push_back(id);
176+
}
177+
178+
137179
static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
138180
Environment* env = Environment::GetCurrent(args);
139181

@@ -170,6 +212,42 @@ void AsyncWrap::GetAsyncId(const FunctionCallbackInfo<Value>& args) {
170212
}
171213

172214

215+
void AsyncWrap::PushAsyncIds(const FunctionCallbackInfo<Value>& args) {
216+
Environment* env = Environment::GetCurrent(args);
217+
// No need for CHECK(IsNumber()) on args because if FromJust() doesn't fail
218+
// then the checks in push_ids() and pop_ids() will.
219+
double async_id = args[0]->NumberValue(env->context()).FromJust();
220+
double trigger_id = args[1]->NumberValue(env->context()).FromJust();
221+
env->async_hooks()->push_ids(async_id, trigger_id);
222+
}
223+
224+
225+
void AsyncWrap::PopAsyncIds(const FunctionCallbackInfo<Value>& args) {
226+
Environment* env = Environment::GetCurrent(args);
227+
double async_id = args[0]->NumberValue(env->context()).FromJust();
228+
args.GetReturnValue().Set(env->async_hooks()->pop_ids(async_id));
229+
}
230+
231+
232+
void AsyncWrap::ClearIdStack(const FunctionCallbackInfo<Value>& args) {
233+
Environment* env = Environment::GetCurrent(args);
234+
env->async_hooks()->clear_id_stack();
235+
}
236+
237+
238+
void AsyncWrap::AsyncReset(const FunctionCallbackInfo<Value>& args) {
239+
AsyncWrap* wrap;
240+
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
241+
wrap->AsyncReset();
242+
}
243+
244+
245+
void AsyncWrap::QueueDestroyId(const FunctionCallbackInfo<Value>& args) {
246+
CHECK(args[0]->IsNumber());
247+
PushBackDestroyId(Environment::GetCurrent(args), args[0]->NumberValue());
248+
}
249+
250+
173251
void AsyncWrap::Initialize(Local<Object> target,
174252
Local<Value> unused,
175253
Local<Context> context) {
@@ -178,6 +256,10 @@ void AsyncWrap::Initialize(Local<Object> target,
178256
HandleScope scope(isolate);
179257

180258
env->SetMethod(target, "setupHooks", SetupHooks);
259+
env->SetMethod(target, "pushAsyncIds", PushAsyncIds);
260+
env->SetMethod(target, "popAsyncIds", PopAsyncIds);
261+
env->SetMethod(target, "clearIdStack", ClearIdStack);
262+
env->SetMethod(target, "addIdToDestroyList", QueueDestroyId);
181263

182264
v8::PropertyAttribute ReadOnlyDontDelete =
183265
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
@@ -252,37 +334,6 @@ void AsyncWrap::Initialize(Local<Object> target,
252334
}
253335

254336

255-
void AsyncWrap::DestroyIdsCb(uv_idle_t* handle) {
256-
uv_idle_stop(handle);
257-
258-
Environment* env = Environment::from_destroy_ids_idle_handle(handle);
259-
260-
HandleScope handle_scope(env->isolate());
261-
Context::Scope context_scope(env->context());
262-
Local<Function> fn = env->async_hooks_destroy_function();
263-
264-
TryCatch try_catch(env->isolate());
265-
266-
std::vector<double> destroy_ids_list;
267-
destroy_ids_list.swap(*env->destroy_ids_list());
268-
for (auto current_id : destroy_ids_list) {
269-
// Want each callback to be cleaned up after itself, instead of cleaning
270-
// them all up after the while() loop completes.
271-
HandleScope scope(env->isolate());
272-
Local<Value> argv = Number::New(env->isolate(), current_id);
273-
MaybeLocal<Value> ret = fn->Call(
274-
env->context(), Undefined(env->isolate()), 1, &argv);
275-
276-
if (ret.IsEmpty()) {
277-
ClearFatalExceptionHandlers(env);
278-
FatalException(env->isolate(), try_catch);
279-
}
280-
}
281-
282-
env->destroy_ids_list()->clear();
283-
}
284-
285-
286337
void LoadAsyncWrapperInfo(Environment* env) {
287338
HeapProfiler* heap_profiler = env->isolate()->GetHeapProfiler();
288339
#define V(PROVIDER) \
@@ -310,21 +361,14 @@ AsyncWrap::AsyncWrap(Environment* env,
310361

311362

312363
AsyncWrap::~AsyncWrap() {
313-
if (env()->async_hooks()->fields()[AsyncHooks::kDestroy] == 0) {
314-
return;
315-
}
316-
317-
if (env()->destroy_ids_list()->empty())
318-
uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb);
319-
320-
env()->destroy_ids_list()->push_back(get_id());
364+
PushBackDestroyId(env(), get_id());
321365
}
322366

323367

324368
// Generalized call for both the constructor and for handles that are pooled
325369
// and reused over their lifetime. This way a new uid can be assigned when
326370
// the resource is pulled out of the pool and put back into use.
327-
void AsyncWrap::Reset() {
371+
void AsyncWrap::AsyncReset() {
328372
AsyncHooks* async_hooks = env()->async_hooks();
329373
async_id_ = env()->new_async_id();
330374
trigger_id_ = env()->get_init_trigger_id();

src/async-wrap.h

+6-3
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,19 @@ class AsyncWrap : public BaseObject {
8585
v8::Local<v8::Context> context);
8686

8787
static void GetAsyncId(const v8::FunctionCallbackInfo<v8::Value>& args);
88-
89-
static void DestroyIdsCb(uv_idle_t* handle);
88+
static void PushAsyncIds(const v8::FunctionCallbackInfo<v8::Value>& args);
89+
static void PopAsyncIds(const v8::FunctionCallbackInfo<v8::Value>& args);
90+
static void ClearIdStack(const v8::FunctionCallbackInfo<v8::Value>& args);
91+
static void AsyncReset(const v8::FunctionCallbackInfo<v8::Value>& args);
92+
static void QueueDestroyId(const v8::FunctionCallbackInfo<v8::Value>& args);
9093

9194
inline ProviderType provider_type() const;
9295

9396
inline double get_id() const;
9497

9598
inline double get_trigger_id() const;
9699

97-
void Reset();
100+
void AsyncReset();
98101

99102
// Only call these within a valid HandleScope.
100103
// TODO(trevnorris): These should return a MaybeLocal.

src/node_http_parser.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ class Parser : public AsyncWrap {
477477
// Should always be called from the same context.
478478
CHECK_EQ(env, parser->env());
479479
// The parser is being reused. Reset the uid and call init() callbacks.
480-
parser->Reset();
480+
parser->AsyncReset();
481481
parser->Init(type);
482482
}
483483

src/tcp_wrap.cc

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ void TCPWrap::Initialize(Local<Object> target,
8686
Null(env->isolate()));
8787

8888
env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId);
89+
env->SetProtoMethod(t, "asyncReset", AsyncWrap::AsyncReset);
8990

9091
env->SetProtoMethod(t, "close", HandleWrap::Close);
9192

test/common/index.js

+59
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,50 @@ exports.enoughTestCpu = Array.isArray(cpus) &&
6464
exports.rootDir = exports.isWindows ? 'c:\\' : '/';
6565
exports.buildType = process.config.target_defaults.default_configuration;
6666

67+
// If env var is set then enable async_hook hooks for all tests.
68+
if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) {
69+
const destroydIdsList = {};
70+
const destroyListList = {};
71+
const initHandles = {};
72+
const async_wrap = process.binding('async_wrap');
73+
74+
process.on('exit', () => {
75+
// itterate through handles to make sure nothing crashes
76+
for (const k in initHandles)
77+
util.inspect(initHandles[k]);
78+
});
79+
80+
const _addIdToDestroyList = async_wrap.addIdToDestroyList;
81+
async_wrap.addIdToDestroyList = function addIdToDestroyList(id) {
82+
if (destroyListList[id] !== undefined) {
83+
process._rawDebug(destroyListList[id]);
84+
process._rawDebug();
85+
throw new Error(`same id added twice (${id})`);
86+
}
87+
destroyListList[id] = new Error().stack;
88+
_addIdToDestroyList(id);
89+
};
90+
91+
require('async_hooks').createHook({
92+
init(id, ty, tr, h) {
93+
if (initHandles[id]) {
94+
throw new Error(`init called twice for same id (${id})`);
95+
}
96+
initHandles[id] = h;
97+
},
98+
before() { },
99+
after() { },
100+
destroy(id) {
101+
if (destroydIdsList[id] !== undefined) {
102+
process._rawDebug(destroydIdsList[id]);
103+
process._rawDebug();
104+
throw new Error(`destroy called for same id (${id})`);
105+
}
106+
destroydIdsList[id] = new Error().stack;
107+
},
108+
}).enable();
109+
}
110+
67111
function rimrafSync(p) {
68112
let st;
69113
try {
@@ -684,3 +728,18 @@ exports.crashOnUnhandledRejection = function() {
684728
process.on('unhandledRejection',
685729
(err) => process.nextTick(() => { throw err; }));
686730
};
731+
732+
exports.getTTYfd = function getTTYfd() {
733+
const tty = require('tty');
734+
let tty_fd = 0;
735+
if (!tty.isatty(tty_fd)) tty_fd++;
736+
else if (!tty.isatty(tty_fd)) tty_fd++;
737+
else if (!tty.isatty(tty_fd)) tty_fd++;
738+
else try {
739+
tty_fd = require('fs').openSync('/dev/tty');
740+
} catch (e) {
741+
// There aren't any tty fd's available to use.
742+
return -1;
743+
}
744+
return tty_fd;
745+
};
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const async_wrap = process.binding('async_wrap');
5+
const assert = require('assert');
6+
const async_hooks = require('async_hooks');
7+
const RUNS = 5;
8+
let test_id = null;
9+
let run_cntr = 0;
10+
let hooks = null;
11+
12+
process.on('beforeExit', common.mustCall(() => {
13+
process.removeAllListeners('uncaughtException');
14+
hooks.disable();
15+
assert.strictEqual(test_id, null);
16+
assert.strictEqual(run_cntr, RUNS);
17+
}));
18+
19+
20+
hooks = async_hooks.createHook({
21+
destroy(id) {
22+
if (id === test_id) {
23+
run_cntr++;
24+
test_id = null;
25+
}
26+
},
27+
}).enable();
28+
29+
30+
(function runner(n) {
31+
assert.strictEqual(test_id, null);
32+
if (n <= 0) return;
33+
34+
test_id = (Math.random() * 1e9) >>> 0;
35+
async_wrap.addIdToDestroyList(test_id);
36+
setImmediate(common.mustCall(runner), n - 1);
37+
})(RUNS);

test/parallel/test-async-wrap-getasyncid.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,24 @@ if (common.hasCrypto) {
218218

219219

220220
{
221-
const tty_wrap = process.binding('tty_wrap');
222-
if (tty_wrap.isTTY(0)) {
223-
testInitialized(new tty_wrap.TTY(0, false), 'TTY');
221+
// Do our best to grab a tty fd.
222+
const tty_fd = common.getTTYfd();
223+
if (tty_fd >= 0) {
224+
const tty_wrap = process.binding('tty_wrap');
225+
// fd may still be invalid, so guard against it.
226+
const handle = (() => {
227+
try {
228+
return new tty_wrap.TTY(tty_fd, false);
229+
} catch (e) {
230+
return null;
231+
}
232+
})();
233+
if (handle !== null)
234+
testInitialized(handle, 'TTY');
235+
else
236+
delete providers.TTYWRAP;
237+
} else {
238+
delete providers.TTYWRAP;
224239
}
225240
}
226241

0 commit comments

Comments
 (0)