Skip to content

Commit

Permalink
async_hooks: initial async_hooks implementation
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
trevnorris authored and addaleax committed May 10, 2017
1 parent c0bde73 commit 7e3a3c9
Show file tree
Hide file tree
Showing 10 changed files with 699 additions and 51 deletions.
488 changes: 488 additions & 0 deletions lib/async_hooks.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions lib/internal/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ function stripShebang(content) {
}

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

function addBuiltinLibsToObject(object) {
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'node_core_target_name%': 'node',
'library_files': [
'lib/internal/bootstrap_node.js',
'lib/async_hooks.js',
'lib/assert.js',
'lib/buffer.js',
'lib/child_process.js',
Expand Down
124 changes: 84 additions & 40 deletions src/async-wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,48 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local<Value> wrapper) {
// end RetainedAsyncInfo


static void DestroyIdsCb(uv_idle_t* handle) {
uv_idle_stop(handle);

Environment* env = Environment::from_destroy_ids_idle_handle(handle);

HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
Local<Function> fn = env->async_hooks_destroy_function();

TryCatch try_catch(env->isolate());

std::vector<double> destroy_ids_list;
destroy_ids_list.swap(*env->destroy_ids_list());
for (auto current_id : destroy_ids_list) {
// Want each callback to be cleaned up after itself, instead of cleaning
// them all up after the while() loop completes.
HandleScope scope(env->isolate());
Local<Value> argv = Number::New(env->isolate(), current_id);
MaybeLocal<Value> ret = fn->Call(
env->context(), Undefined(env->isolate()), 1, &argv);

if (ret.IsEmpty()) {
ClearFatalExceptionHandlers(env);
FatalException(env->isolate(), try_catch);
}
}

env->destroy_ids_list()->clear();
}


static void PushBackDestroyId(Environment* env, double id) {
if (env->async_hooks()->fields()[AsyncHooks::kDestroy] == 0)
return;

if (env->destroy_ids_list()->empty())
uv_idle_start(env->destroy_ids_idle_handle(), DestroyIdsCb);

env->destroy_ids_list()->push_back(id);
}


static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

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


void AsyncWrap::PushAsyncIds(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
// No need for CHECK(IsNumber()) on args because if FromJust() doesn't fail
// then the checks in push_ids() and pop_ids() will.
double async_id = args[0]->NumberValue(env->context()).FromJust();
double trigger_id = args[1]->NumberValue(env->context()).FromJust();
env->async_hooks()->push_ids(async_id, trigger_id);
}


void AsyncWrap::PopAsyncIds(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
double async_id = args[0]->NumberValue(env->context()).FromJust();
args.GetReturnValue().Set(env->async_hooks()->pop_ids(async_id));
}


void AsyncWrap::ClearIdStack(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
env->async_hooks()->clear_id_stack();
}


void AsyncWrap::AsyncReset(const FunctionCallbackInfo<Value>& args) {
AsyncWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
wrap->AsyncReset();
}


void AsyncWrap::QueueDestroyId(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsNumber());
PushBackDestroyId(Environment::GetCurrent(args), args[0]->NumberValue());
}


void AsyncWrap::Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {
Expand All @@ -178,6 +256,10 @@ void AsyncWrap::Initialize(Local<Object> target,
HandleScope scope(isolate);

env->SetMethod(target, "setupHooks", SetupHooks);
env->SetMethod(target, "pushAsyncIds", PushAsyncIds);
env->SetMethod(target, "popAsyncIds", PopAsyncIds);
env->SetMethod(target, "clearIdStack", ClearIdStack);
env->SetMethod(target, "addIdToDestroyList", QueueDestroyId);

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


void AsyncWrap::DestroyIdsCb(uv_idle_t* handle) {
uv_idle_stop(handle);

Environment* env = Environment::from_destroy_ids_idle_handle(handle);

HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
Local<Function> fn = env->async_hooks_destroy_function();

TryCatch try_catch(env->isolate());

std::vector<double> destroy_ids_list;
destroy_ids_list.swap(*env->destroy_ids_list());
for (auto current_id : destroy_ids_list) {
// Want each callback to be cleaned up after itself, instead of cleaning
// them all up after the while() loop completes.
HandleScope scope(env->isolate());
Local<Value> argv = Number::New(env->isolate(), current_id);
MaybeLocal<Value> ret = fn->Call(
env->context(), Undefined(env->isolate()), 1, &argv);

if (ret.IsEmpty()) {
ClearFatalExceptionHandlers(env);
FatalException(env->isolate(), try_catch);
}
}

env->destroy_ids_list()->clear();
}


void LoadAsyncWrapperInfo(Environment* env) {
HeapProfiler* heap_profiler = env->isolate()->GetHeapProfiler();
#define V(PROVIDER) \
Expand Down Expand Up @@ -310,21 +361,14 @@ AsyncWrap::AsyncWrap(Environment* env,


AsyncWrap::~AsyncWrap() {
if (env()->async_hooks()->fields()[AsyncHooks::kDestroy] == 0) {
return;
}

if (env()->destroy_ids_list()->empty())
uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb);

env()->destroy_ids_list()->push_back(get_id());
PushBackDestroyId(env(), get_id());
}


// Generalized call for both the constructor and for handles that are pooled
// and reused over their lifetime. This way a new uid can be assigned when
// the resource is pulled out of the pool and put back into use.
void AsyncWrap::Reset() {
void AsyncWrap::AsyncReset() {
AsyncHooks* async_hooks = env()->async_hooks();
async_id_ = env()->new_async_id();
trigger_id_ = env()->get_init_trigger_id();
Expand Down
9 changes: 6 additions & 3 deletions src/async-wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,19 @@ class AsyncWrap : public BaseObject {
v8::Local<v8::Context> context);

static void GetAsyncId(const v8::FunctionCallbackInfo<v8::Value>& args);

static void DestroyIdsCb(uv_idle_t* handle);
static void PushAsyncIds(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PopAsyncIds(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ClearIdStack(const v8::FunctionCallbackInfo<v8::Value>& args);
static void AsyncReset(const v8::FunctionCallbackInfo<v8::Value>& args);
static void QueueDestroyId(const v8::FunctionCallbackInfo<v8::Value>& args);

inline ProviderType provider_type() const;

inline double get_id() const;

inline double get_trigger_id() const;

void Reset();
void AsyncReset();

// Only call these within a valid HandleScope.
// TODO(trevnorris): These should return a MaybeLocal.
Expand Down
2 changes: 1 addition & 1 deletion src/node_http_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ class Parser : public AsyncWrap {
// Should always be called from the same context.
CHECK_EQ(env, parser->env());
// The parser is being reused. Reset the uid and call init() callbacks.
parser->Reset();
parser->AsyncReset();
parser->Init(type);
}

Expand Down
1 change: 1 addition & 0 deletions src/tcp_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ void TCPWrap::Initialize(Local<Object> target,
Null(env->isolate()));

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

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

Expand Down
59 changes: 59 additions & 0 deletions test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,50 @@ exports.enoughTestCpu = Array.isArray(cpus) &&
exports.rootDir = exports.isWindows ? 'c:\\' : '/';
exports.buildType = process.config.target_defaults.default_configuration;

// If env var is set then enable async_hook hooks for all tests.
if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) {
const destroydIdsList = {};
const destroyListList = {};
const initHandles = {};
const async_wrap = process.binding('async_wrap');

process.on('exit', () => {
// itterate through handles to make sure nothing crashes
for (const k in initHandles)
util.inspect(initHandles[k]);
});

const _addIdToDestroyList = async_wrap.addIdToDestroyList;
async_wrap.addIdToDestroyList = function addIdToDestroyList(id) {
if (destroyListList[id] !== undefined) {
process._rawDebug(destroyListList[id]);
process._rawDebug();
throw new Error(`same id added twice (${id})`);
}
destroyListList[id] = new Error().stack;
_addIdToDestroyList(id);
};

require('async_hooks').createHook({
init(id, ty, tr, h) {
if (initHandles[id]) {
throw new Error(`init called twice for same id (${id})`);
}
initHandles[id] = h;
},
before() { },
after() { },
destroy(id) {
if (destroydIdsList[id] !== undefined) {
process._rawDebug(destroydIdsList[id]);
process._rawDebug();
throw new Error(`destroy called for same id (${id})`);
}
destroydIdsList[id] = new Error().stack;
},
}).enable();
}

function rimrafSync(p) {
let st;
try {
Expand Down Expand Up @@ -684,3 +728,18 @@ exports.crashOnUnhandledRejection = function() {
process.on('unhandledRejection',
(err) => process.nextTick(() => { throw err; }));
};

exports.getTTYfd = function getTTYfd() {
const tty = require('tty');
let tty_fd = 0;
if (!tty.isatty(tty_fd)) tty_fd++;
else if (!tty.isatty(tty_fd)) tty_fd++;
else if (!tty.isatty(tty_fd)) tty_fd++;
else try {
tty_fd = require('fs').openSync('/dev/tty');
} catch (e) {
// There aren't any tty fd's available to use.
return -1;
}
return tty_fd;
};
37 changes: 37 additions & 0 deletions test/parallel/test-async-wrap-destroyid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

const common = require('../common');
const async_wrap = process.binding('async_wrap');
const assert = require('assert');
const async_hooks = require('async_hooks');
const RUNS = 5;
let test_id = null;
let run_cntr = 0;
let hooks = null;

process.on('beforeExit', common.mustCall(() => {
process.removeAllListeners('uncaughtException');
hooks.disable();
assert.strictEqual(test_id, null);
assert.strictEqual(run_cntr, RUNS);
}));


hooks = async_hooks.createHook({
destroy(id) {
if (id === test_id) {
run_cntr++;
test_id = null;
}
},
}).enable();


(function runner(n) {
assert.strictEqual(test_id, null);
if (n <= 0) return;

test_id = (Math.random() * 1e9) >>> 0;
async_wrap.addIdToDestroyList(test_id);
setImmediate(common.mustCall(runner), n - 1);
})(RUNS);
21 changes: 18 additions & 3 deletions test/parallel/test-async-wrap-getasyncid.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,24 @@ if (common.hasCrypto) {


{
const tty_wrap = process.binding('tty_wrap');
if (tty_wrap.isTTY(0)) {
testInitialized(new tty_wrap.TTY(0, false), 'TTY');
// Do our best to grab a tty fd.
const tty_fd = common.getTTYfd();
if (tty_fd >= 0) {
const tty_wrap = process.binding('tty_wrap');
// fd may still be invalid, so guard against it.
const handle = (() => {
try {
return new tty_wrap.TTY(tty_fd, false);
} catch (e) {
return null;
}
})();
if (handle !== null)
testInitialized(handle, 'TTY');
else
delete providers.TTYWRAP;
} else {
delete providers.TTYWRAP;
}
}

Expand Down

0 comments on commit 7e3a3c9

Please sign in to comment.