Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Commit

Permalink
nextTick: Preserve depth in error/reentry cases
Browse files Browse the repository at this point in the history
When there is an error that is thrown in a nextTick function, which is
then handled by a domain or other process.on('uncaughtException')
handler, if the error handler *also* adds a nextTick and triggers
multiple MakeCallback events (ie, by doing some I/O), then it would
skip over the tickDepth check, resulting in an infinite spin.

Solution: Check the tickDepth at the start of the tick processing, and
preserve it when we are cleaning up in the error case or exiting early
in the re-entry case.

In order to make sure that tick callbacks are *eventually* handled, any
callback triggered by the underlying spinner in libuv will be processed
as if starting from a tick depth of 0.
  • Loading branch information
isaacs committed Jul 17, 2012
1 parent a52a44e commit bb6033a
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 7 deletions.
4 changes: 3 additions & 1 deletion src/node.cc
Expand Up @@ -247,7 +247,9 @@ static void Tick(void) {

TryCatch try_catch;

cb->Call(process, 0, NULL);
// Let the tick callback know that this is coming from the spinner
Handle<Value> argv[] = { True() };
cb->Call(process, ARRAY_SIZE(argv), argv);

if (try_catch.HasCaught()) {
FatalException(try_catch);
Expand Down
39 changes: 33 additions & 6 deletions src/node.js
Expand Up @@ -260,8 +260,8 @@
// limit is hit, which is not ideal, but not terrible.
process.maxTickDepth = 1000;

function tickDone() {
tickDepth = 0;
function tickDone(td) {

This comment has been minimized.

Copy link
@bnoordhuis

bnoordhuis Jul 18, 2012

Member

s/td/tickDepth_/ ?

tickDepth = td || 0;
nextTickQueue.splice(0, nextTickIndex);
nextTickIndex = 0;
inTick = false;
Expand All @@ -270,12 +270,35 @@
}
}

process._tickCallback = function() {
process._tickCallback = function(fromSpinner) {

// if you add a nextTick in a domain's error handler, then
// it's possible to cycle indefinitely. Normally, the tickDone
// in the finally{} block below will prevent this, however if
// that error handler ALSO triggers multiple MakeCallbacks, then
// it'll try to keep clearing the queue, since the finally block
// fires *before* the error hits the top level and is handled.
if (tickDepth >= process.maxTickDepth) {
if (fromSpinner) {
// coming in from the event queue. reset.
tickDepth = 0;
} else {
if (nextTickQueue.length) {
process._needTickCallback();
}
return;
}
}

if (!nextTickQueue.length) return tickDone();

if (inTick) return;
inTick = true;

// always do this at least once. otherwise if process.maxTickDepth
// is set to some negative value, we'd never process any of them.
// is set to some negative value, or if there were repeated errors
// preventing tickDepth from being cleared, we'd never process any
// of them.
do {
tickDepth++;
var nextTickLength = nextTickQueue.length;
Expand All @@ -292,7 +315,9 @@
callback();
threw = false;
} finally {
if (threw) tickDone();
// finallys fire before the error hits the top level,

This comment has been minimized.

Copy link
@bnoordhuis

bnoordhuis Jul 18, 2012

Member

s/finallys/finally/

// so we can't clear the tickDepth at this point.
if (threw) tickDone(tickDepth);
}
if (tock.domain) {
tock.domain.exit();
Expand All @@ -316,7 +341,9 @@
var tock = { callback: callback };
if (process.domain) tock.domain = process.domain;
nextTickQueue.push(tock);
process._needTickCallback();
if (nextTickQueue.length) {
process._needTickCallback();
}
};
};

Expand Down
4 changes: 4 additions & 0 deletions test/simple/test-domain-stack.js
Expand Up @@ -44,3 +44,7 @@ var foo = a.bind(function() {
for (var i = 0; i < 1000; i++) {
process.nextTick(foo);
}

process.on('exit', function(c) {
if (!c) console.log('ok');
});
69 changes: 69 additions & 0 deletions test/simple/test-next-tick-error-spin.js
@@ -0,0 +1,69 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var common = require('../common');
var assert = require('assert');

if (process.argv[2] !== 'child') {
var spawn = require('child_process').spawn;
var child = spawn(process.execPath, [__filename, 'child'], {
stdio: 'pipe'//'inherit'
});
var timer = setTimeout(function() {
throw new Error('child is hung');
}, 500);
child.on('exit', function(code) {
console.error('ok');
assert(!code);
clearTimeout(timer);
});
} else {

var domain = require('domain');
var d = domain.create();
process.maxTickDepth = 10;

// in the error handler, we trigger several MakeCallback events
d.on('error', function(e) {
console.log('a')
console.log('b')
console.log('c')
console.log('d')
console.log('e')
f();
});

function f() {
process.nextTick(function() {
d.run(function() {
throw(new Error('x'));
});
});
}

f();
setTimeout(function () {
console.error('broke in!');
//process.stdout.close();
//process.stderr.close();
process.exit(0);
});
}

0 comments on commit bb6033a

Please sign in to comment.