From 4e5fe2d45aef5aacf4a6c6c2db28f0b9cc64f123 Mon Sep 17 00:00:00 2001 From: isaacs Date: Fri, 13 Jul 2012 18:00:49 -0700 Subject: [PATCH] nextTick: Handle tick callbacks after each tick --- src/node.js | 61 ++++++++++++------ .../test-next-tick-intentional-starvation.js | 64 +++++++++++++++++++ test/simple/test-process-active-wraps.js | 9 ++- 3 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 test/simple/test-next-tick-intentional-starvation.js diff --git a/src/node.js b/src/node.js index 029beb6c0e..a86d723029 100644 --- a/src/node.js +++ b/src/node.js @@ -239,6 +239,8 @@ if (domain) domain.exit(); + // process the nextTicks after each time we get called. + process._tickCallback(); return ret; }; }; @@ -247,8 +249,19 @@ var nextTickQueue = []; var nextTickIndex = 0; var inTick = false; + var tickDepth = 0; + + // the maximum number of times it'll process something like + // nextTick(function f(){nextTick(f)}) + // It's unlikely, but not illegal, to hit this limit. When + // that happens, it yields to libuv's tick spinner. + // This is a loop counter, not a stack depth, so we aren't using + // up lots of memory here. I/O can sneak in before nextTick if this + // limit is hit, which is not ideal, but not terrible. + process.maxTickDepth = 1000; function tickDone() { + tickDepth = 0; nextTickQueue.splice(0, nextTickIndex); nextTickIndex = 0; inTick = false; @@ -259,28 +272,38 @@ process._tickCallback = function() { if (inTick) return; - var nextTickLength = nextTickQueue.length; - if (nextTickLength === 0) return; inTick = true; - while (nextTickIndex < nextTickLength) { - var tock = nextTickQueue[nextTickIndex++]; - var callback = tock.callback; - if (tock.domain) { - if (tock.domain._disposed) continue; - tock.domain.enter(); - } - var threw = true; - try { - callback(); - threw = false; - } finally { - if (threw) tickDone(); - } - if (tock.domain) { - tock.domain.exit(); + // always do this at least once. otherwise if process.maxTickDepth + // is set to some negative value, we'd never process any of them. + do { + tickDepth++; + var nextTickLength = nextTickQueue.length; + if (nextTickLength === 0) return tickDone(); + while (nextTickIndex < nextTickLength) { + var tock = nextTickQueue[nextTickIndex++]; + var callback = tock.callback; + if (tock.domain) { + if (tock.domain._disposed) continue; + tock.domain.enter(); + } + var threw = true; + try { + callback(); + threw = false; + } finally { + if (threw) tickDone(); + } + if (tock.domain) { + tock.domain.exit(); + } } - } + nextTickQueue.splice(0, nextTickIndex); + nextTickIndex = 0; + + // continue until the max depth or we run out of tocks. + } while (tickDepth < process.maxTickDepth && + nextTickQueue.length > 0); tickDone(); }; diff --git a/test/simple/test-next-tick-intentional-starvation.js b/test/simple/test-next-tick-intentional-starvation.js new file mode 100644 index 0000000000..1a76b27e44 --- /dev/null +++ b/test/simple/test-next-tick-intentional-starvation.js @@ -0,0 +1,64 @@ +// 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'); + +// this is the inverse of test-next-tick-starvation. +// it verifies that process.nextTick will *always* come before other +// events, up to the limit of the process.maxTickDepth value. + +// WARNING: unsafe! +process.maxTickDepth = Infinity; + +var ran = false; +var starved = false; +var start = +new Date(); +var timerRan = false; + +function spin() { + ran = true; + var now = +new Date(); + if (now - start > 100) { + console.log('The timer is starving, just as we planned.'); + starved = true; + + // now let it out. + return; + } + + process.nextTick(spin); +} + +function onTimeout() { + if (!starved) throw new Error('The timer escaped!'); + console.log('The timer ran once the ban was lifted'); + timerRan = true; +} + +spin(); +setTimeout(onTimeout, 50); + +process.on('exit', function() { + assert.ok(ran); + assert.ok(starved); + assert.ok(timerRan); +}); diff --git a/test/simple/test-process-active-wraps.js b/test/simple/test-process-active-wraps.js index 3fdf993c10..fe68e15f05 100644 --- a/test/simple/test-process-active-wraps.js +++ b/test/simple/test-process-active-wraps.js @@ -46,11 +46,16 @@ function expect(activeHandles, activeRequests) { expect(2, 1); // client handle doesn't shut down until next tick })(); +// Force the nextTicks to be deferred to a later time. +process.maxTickDepth = 1; + process.nextTick(function() { process.nextTick(function() { process.nextTick(function() { - // the handles should be gone but the connect req could still be alive - assert.equal(process._getActiveHandles().length, 0); + process.nextTick(function() { + // the handles should be gone but the connect req could still be alive + assert.equal(process._getActiveHandles().length, 0); + }); }); }); });