Skip to content
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: simplify fatalError #14051

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions lib/async_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,11 @@ async_wrap.setupHooks({ init,
destroy: emitDestroyN });

// Used to fatally abort the process if a callback throws.
function fatalError(e) {
Copy link
Contributor

@refack refack Jul 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is everyone trying to kill this 😨
I like it as a way to bail from a callback or a Promise.catch
Ref: #13839 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just because it is useful for Promise.catch doesn't mean that is useful here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably. I'll copy it to test/common/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Letting an error bubble with try {} finally {} have always been the standard way of handling throws.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except what async_hooks is doing isn't standard. This is a guard to prevent an abort from C++ if it detects stack corruption that can likely result from the user try catching/Promise wrapping the call, and to get useful core dumps. It also doesn't follow how this is handled in C++ (https://github.com/nodejs/node/blob/v8.1.3/src/env-inl.h#L144-L160). I'm a big -1 on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "exporting FatalException" do you mean expose it to JS?

Copy link
Contributor

@trevnorris trevnorris Jul 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anything I'd like to export the code in Environment::AsyncHooks::pop_ids() and use that as the uniform way to exit the process.

EDIT: Except it should show a JS stack instead of a C++ stack.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "exporting FatalException" do you mean expose it to JS?

Yes. At the very least I would like a unified way of making a fatal exception. Currently, there are a couple of different implementations floating around in nodecore. It makes things like --abort-on-uncaught-exception difficult to test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndreasMadsen

At the very least I would like a unified way of making a fatal exception.

Sounds good to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So area this is currently a WIP?

if (typeof e.stack === 'string') {
process._rawDebug(e.stack);
} else {
const o = { message: e };
Error.captureStackTrace(o, fatalError);
process._rawDebug(o.stack);
}
if (process.execArgv.some(
(e) => /^--abort[_-]on[_-]uncaught[_-]exception$/.test(e))) {
process.abort();
}
process.exit(1);
// This is JavaScript version of the ClearFatalExceptionHandlers function
// in C++.
function clearFatalExceptionHandlers(e) {
process.domain = undefined;
process._events.uncaughtException = undefined;
}


Expand Down Expand Up @@ -345,15 +337,17 @@ function emitInitS(asyncId, type, triggerAsyncId, resource) {

function emitBeforeN(asyncId) {
processing_hook = true;
var didThrow = true;
// Use a single try/catch for all hook to avoid setting up one per iteration.
try {
for (var i = 0; i < active_hooks_array.length; i++) {
if (typeof active_hooks_array[i][before_symbol] === 'function') {
active_hooks_array[i][before_symbol](asyncId);
}
}
} catch (e) {
fatalError(e);
didThrow = false;
} finally {
if (didThrow) clearFatalExceptionHandlers();
}
processing_hook = false;

Expand All @@ -371,8 +365,11 @@ function emitBeforeS(asyncId, triggerAsyncId = asyncId) {

// Validate the ids.
if (asyncId < 0 || triggerAsyncId < 0) {
fatalError('before(): asyncId or triggerAsyncId is less than zero ' +
`(asyncId: ${asyncId}, triggerAsyncId: ${triggerAsyncId})`);
clearFatalExceptionHandlers();
throw new Error(
'before(): asyncId or triggerAsyncId is less than zero ' +
`(asyncId: ${asyncId}, triggerAsyncId: ${triggerAsyncId})`
);
}

pushAsyncIds(asyncId, triggerAsyncId);
Expand All @@ -387,15 +384,17 @@ function emitBeforeS(asyncId, triggerAsyncId = asyncId) {
// this is called.
function emitAfterN(asyncId) {
processing_hook = true;
var didThrow = true;
// Use a single try/catch for all hook to avoid setting up one per iteration.
try {
for (var i = 0; i < active_hooks_array.length; i++) {
if (typeof active_hooks_array[i][after_symbol] === 'function') {
active_hooks_array[i][after_symbol](asyncId);
}
}
didThrow = false;
} catch (e) {
fatalError(e);
if (didThrow) clearFatalExceptionHandlers();
}
processing_hook = false;

Expand Down Expand Up @@ -427,15 +426,17 @@ function emitDestroyS(asyncId) {

function emitDestroyN(asyncId) {
processing_hook = true;
var didThrow = true;
// Use a single try/catch for all hook to avoid setting up one per iteration.
try {
for (var i = 0; i < active_hooks_array.length; i++) {
if (typeof active_hooks_array[i][destroy_symbol] === 'function') {
active_hooks_array[i][destroy_symbol](asyncId);
}
}
} catch (e) {
fatalError(e);
didThrow = false;
} finally {
if (didThrow) clearFatalExceptionHandlers();
}
processing_hook = false;

Expand All @@ -460,6 +461,7 @@ function emitDestroyN(asyncId) {
// exceptions.
function init(asyncId, type, triggerAsyncId, resource) {
processing_hook = true;
var didThrow = true;
// Use a single try/catch for all hook to avoid setting up one per iteration.
try {
for (var i = 0; i < active_hooks_array.length; i++) {
Expand All @@ -470,8 +472,8 @@ function init(asyncId, type, triggerAsyncId, resource) {
);
}
}
} catch (e) {
fatalError(e);
} finally {
if (didThrow) clearFatalExceptionHandlers();
}
processing_hook = false;
}
Expand Down
10 changes: 7 additions & 3 deletions test/async-hooks/test-callback-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const async_hooks = require('async_hooks');
const initHooks = require('./init-hooks');

const arg = process.argv[2];
if (arg) {
process.on('uncaughtException', common.mustNotCall());
}

switch (arg) {
case 'test_init_callback':
initHooks({
Expand Down Expand Up @@ -50,7 +54,7 @@ assert.ok(!arg);
console.time('end case 1');
const child = spawnSync(process.execPath, [__filename, 'test_init_callback']);
assert.ifError(child.error);
const test_init_first_line = child.stderr.toString().split(/[\r\n]+/g)[0];
const test_init_first_line = child.stderr.toString().split(/[\r\n]+/g)[3];
assert.strictEqual(test_init_first_line, 'Error: test_init_callback');
assert.strictEqual(child.status, 1);
console.timeEnd('end case 1');
Expand All @@ -61,7 +65,7 @@ assert.ok(!arg);
console.time('end case 2');
const child = spawnSync(process.execPath, [__filename, 'test_callback']);
assert.ifError(child.error);
const test_callback_first_line = child.stderr.toString().split(/[\r\n]+/g)[0];
const test_callback_first_line = child.stderr.toString().split(/[\r\n]+/g)[3];
assert.strictEqual(test_callback_first_line, 'Error: test_callback');
assert.strictEqual(child.status, 1);
console.timeEnd('end case 2');
Expand Down Expand Up @@ -115,6 +119,6 @@ assert.ok(!arg);
assert.strictEqual(stdout, '');
const firstLineStderr = stderr.split(/[\r\n]+/g)[0].trim();
assert.strictEqual(firstLineStderr, 'Error: test_callback_abort');
console.timeEnd('end case 3');
});
console.timeEnd('end case 3');
}
4 changes: 2 additions & 2 deletions test/async-hooks/test-emit-before-after.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ switch (process.argv[2]) {
}

const c1 = spawnSync(process.execPath, [__filename, 'test_invalid_async_id']);
assert.strictEqual(c1.stderr.toString().split(/[\r\n]+/g)[0],
assert.strictEqual(c1.stderr.toString().split(/[\r\n]+/g)[3],
'Error: before(): asyncId or triggerAsyncId is less than ' +
'zero (asyncId: -1, triggerAsyncId: -1)');
assert.strictEqual(c1.status, 1);

const c2 = spawnSync(process.execPath, [__filename, 'test_invalid_trigger_id']);
assert.strictEqual(c2.stderr.toString().split(/[\r\n]+/g)[0],
assert.strictEqual(c2.stderr.toString().split(/[\r\n]+/g)[3],
'Error: before(): asyncId or triggerAsyncId is less than ' +
'zero (asyncId: 1, triggerAsyncId: -1)');
assert.strictEqual(c2.status, 1);
Expand Down