mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
caf0b36de3
Calling this.unref() during the callback of SetTimeout caused the callback to get executed twice because unref() didn't expect to be called during that time and did not stop the ref()ed Timeout but did start a new timer. This commit prevents the new timer creation when the callback was already called. Fixes: https://github.com/iojs/io.js/issues/1191 Reviewed-by: Trevor Norris <trev.norris@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> PR-URL: https://github.com/iojs/io.js/pull/1231
575 lines
13 KiB
JavaScript
575 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const Timer = process.binding('timer_wrap').Timer;
|
|
const L = require('_linklist');
|
|
const assert = require('assert').ok;
|
|
const util = require('util');
|
|
const debug = util.debuglog('timer');
|
|
const kOnTimeout = Timer.kOnTimeout | 0;
|
|
|
|
// Timeout values > TIMEOUT_MAX are set to 1.
|
|
const TIMEOUT_MAX = 2147483647; // 2^31-1
|
|
|
|
// IDLE TIMEOUTS
|
|
//
|
|
// Because often many sockets will have the same idle timeout we will not
|
|
// use one timeout watcher per item. It is too much overhead. Instead
|
|
// we'll use a single watcher for all sockets with the same timeout value
|
|
// and a linked list. This technique is described in the libev manual:
|
|
// http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#Be_smart_about_timeouts
|
|
|
|
// Object containing all lists, timers
|
|
// key = time in milliseconds
|
|
// value = list
|
|
var lists = {};
|
|
|
|
// the main function - creates lists on demand and the watchers associated
|
|
// with them.
|
|
function insert(item, msecs) {
|
|
item._idleStart = Timer.now();
|
|
item._idleTimeout = msecs;
|
|
|
|
if (msecs < 0) return;
|
|
|
|
var list;
|
|
|
|
if (lists[msecs]) {
|
|
list = lists[msecs];
|
|
} else {
|
|
list = new Timer();
|
|
list.start(msecs, 0);
|
|
|
|
L.init(list);
|
|
|
|
lists[msecs] = list;
|
|
list.msecs = msecs;
|
|
list[kOnTimeout] = listOnTimeout;
|
|
}
|
|
|
|
L.append(list, item);
|
|
assert(!L.isEmpty(list)); // list is not empty
|
|
}
|
|
|
|
function listOnTimeout() {
|
|
var msecs = this.msecs;
|
|
var list = this;
|
|
|
|
debug('timeout callback %d', msecs);
|
|
|
|
var now = Timer.now();
|
|
debug('now: %s', now);
|
|
|
|
var diff, first, threw;
|
|
while (first = L.peek(list)) {
|
|
diff = now - first._idleStart;
|
|
if (diff < msecs) {
|
|
list.start(msecs - diff, 0);
|
|
debug('%d list wait because diff is %d', msecs, diff);
|
|
return;
|
|
} else {
|
|
L.remove(first);
|
|
assert(first !== L.peek(list));
|
|
|
|
if (!first._onTimeout) continue;
|
|
|
|
// v0.4 compatibility: if the timer callback throws and the
|
|
// domain or uncaughtException handler ignore the exception,
|
|
// other timers that expire on this tick should still run.
|
|
//
|
|
// https://github.com/joyent/node/issues/2631
|
|
var domain = first.domain;
|
|
if (domain && domain._disposed)
|
|
continue;
|
|
|
|
try {
|
|
if (domain)
|
|
domain.enter();
|
|
threw = true;
|
|
first._called = true;
|
|
first._onTimeout();
|
|
if (domain)
|
|
domain.exit();
|
|
threw = false;
|
|
} finally {
|
|
if (threw) {
|
|
// We need to continue processing after domain error handling
|
|
// is complete, but not by using whatever domain was left over
|
|
// when the timeout threw its exception.
|
|
var oldDomain = process.domain;
|
|
process.domain = null;
|
|
process.nextTick(function() {
|
|
list[kOnTimeout]();
|
|
});
|
|
process.domain = oldDomain;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
debug('%d list empty', msecs);
|
|
assert(L.isEmpty(list));
|
|
list.close();
|
|
delete lists[msecs];
|
|
}
|
|
|
|
|
|
const unenroll = exports.unenroll = function(item) {
|
|
L.remove(item);
|
|
|
|
var list = lists[item._idleTimeout];
|
|
// if empty then stop the watcher
|
|
debug('unenroll');
|
|
if (list && L.isEmpty(list)) {
|
|
debug('unenroll: list empty');
|
|
list.close();
|
|
delete lists[item._idleTimeout];
|
|
}
|
|
// if active is called later, then we want to make sure not to insert again
|
|
item._idleTimeout = -1;
|
|
};
|
|
|
|
|
|
// Does not start the time, just sets up the members needed.
|
|
exports.enroll = function(item, msecs) {
|
|
if (typeof msecs !== 'number') {
|
|
throw new TypeError('msecs must be a number');
|
|
}
|
|
|
|
if (msecs < 0 || !isFinite(msecs)) {
|
|
throw new RangeError('msecs must be a non-negative finite number');
|
|
}
|
|
|
|
// if this item was already in a list somewhere
|
|
// then we should unenroll it from that
|
|
if (item._idleNext) unenroll(item);
|
|
|
|
// Ensure that msecs fits into signed int32
|
|
if (msecs > TIMEOUT_MAX) {
|
|
msecs = TIMEOUT_MAX;
|
|
}
|
|
|
|
item._idleTimeout = msecs;
|
|
L.init(item);
|
|
};
|
|
|
|
|
|
// call this whenever the item is active (not idle)
|
|
// it will reset its timeout.
|
|
exports.active = function(item) {
|
|
var msecs = item._idleTimeout;
|
|
if (msecs >= 0) {
|
|
var list = lists[msecs];
|
|
if (!list || L.isEmpty(list)) {
|
|
insert(item, msecs);
|
|
} else {
|
|
item._idleStart = Timer.now();
|
|
L.append(list, item);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/*
|
|
* DOM-style timers
|
|
*/
|
|
|
|
|
|
exports.setTimeout = function(callback, after) {
|
|
after *= 1; // coalesce to number or NaN
|
|
|
|
if (!(after >= 1 && after <= TIMEOUT_MAX)) {
|
|
after = 1; // schedule on next tick, follows browser behaviour
|
|
}
|
|
|
|
var timer = new Timeout(after);
|
|
var length = arguments.length;
|
|
var ontimeout = callback;
|
|
switch (length) {
|
|
// fast cases
|
|
case 0:
|
|
case 1:
|
|
case 2:
|
|
break;
|
|
case 3:
|
|
ontimeout = callback.bind(timer, arguments[2]);
|
|
break;
|
|
case 4:
|
|
ontimeout = callback.bind(timer, arguments[2], arguments[3]);
|
|
break;
|
|
case 5:
|
|
ontimeout =
|
|
callback.bind(timer, arguments[2], arguments[3], arguments[4]);
|
|
break;
|
|
// slow case
|
|
default:
|
|
var args = new Array(length - 2);
|
|
for (var i = 2; i < length; i++)
|
|
args[i - 2] = arguments[i];
|
|
ontimeout = callback.apply.bind(callback, timer, args);
|
|
break;
|
|
}
|
|
timer._onTimeout = ontimeout;
|
|
|
|
if (process.domain) timer.domain = process.domain;
|
|
|
|
exports.active(timer);
|
|
|
|
return timer;
|
|
};
|
|
|
|
|
|
exports.clearTimeout = function(timer) {
|
|
if (timer && (timer[kOnTimeout] || timer._onTimeout)) {
|
|
timer[kOnTimeout] = timer._onTimeout = null;
|
|
if (timer instanceof Timeout) {
|
|
timer.close(); // for after === 0
|
|
} else {
|
|
exports.unenroll(timer);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
exports.setInterval = function(callback, repeat) {
|
|
repeat *= 1; // coalesce to number or NaN
|
|
|
|
if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) {
|
|
repeat = 1; // schedule on next tick, follows browser behaviour
|
|
}
|
|
|
|
var timer = new Timeout(repeat);
|
|
var length = arguments.length;
|
|
var ontimeout = callback;
|
|
switch (length) {
|
|
case 0:
|
|
case 1:
|
|
case 2:
|
|
break;
|
|
case 3:
|
|
ontimeout = callback.bind(timer, arguments[2]);
|
|
break;
|
|
case 4:
|
|
ontimeout = callback.bind(timer, arguments[2], arguments[3]);
|
|
break;
|
|
case 5:
|
|
ontimeout =
|
|
callback.bind(timer, arguments[2], arguments[3], arguments[4]);
|
|
break;
|
|
default:
|
|
var args = new Array(length - 2);
|
|
for (var i = 2; i < length; i += 1)
|
|
args[i - 2] = arguments[i];
|
|
ontimeout = callback.apply.bind(callback, timer, args);
|
|
break;
|
|
}
|
|
timer._onTimeout = wrapper;
|
|
timer._repeat = ontimeout;
|
|
|
|
if (process.domain) timer.domain = process.domain;
|
|
exports.active(timer);
|
|
|
|
return timer;
|
|
|
|
function wrapper() {
|
|
timer._repeat.call(this);
|
|
// If callback called clearInterval().
|
|
if (timer._repeat === null) return;
|
|
// If timer is unref'd (or was - it's permanently removed from the list.)
|
|
if (this._handle) {
|
|
this._handle.start(repeat, 0);
|
|
} else {
|
|
timer._idleTimeout = repeat;
|
|
exports.active(timer);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
exports.clearInterval = function(timer) {
|
|
if (timer && timer._repeat) {
|
|
timer._repeat = false;
|
|
clearTimeout(timer);
|
|
}
|
|
};
|
|
|
|
|
|
const Timeout = function(after) {
|
|
this._called = false;
|
|
this._idleTimeout = after;
|
|
this._idlePrev = this;
|
|
this._idleNext = this;
|
|
this._idleStart = null;
|
|
this._onTimeout = null;
|
|
this._repeat = false;
|
|
};
|
|
|
|
Timeout.prototype.unref = function() {
|
|
if (this._handle) {
|
|
this._handle.unref();
|
|
} else if (typeof(this._onTimeout) === 'function') {
|
|
var now = Timer.now();
|
|
if (!this._idleStart) this._idleStart = now;
|
|
var delay = this._idleStart + this._idleTimeout - now;
|
|
if (delay < 0) delay = 0;
|
|
exports.unenroll(this);
|
|
|
|
// Prevent running cb again when unref() is called during the same cb
|
|
if (this._called && !this._repeat) return;
|
|
|
|
this._handle = new Timer();
|
|
this._handle[kOnTimeout] = this._onTimeout;
|
|
this._handle.start(delay, 0);
|
|
this._handle.domain = this.domain;
|
|
this._handle.unref();
|
|
}
|
|
};
|
|
|
|
Timeout.prototype.ref = function() {
|
|
if (this._handle)
|
|
this._handle.ref();
|
|
};
|
|
|
|
Timeout.prototype.close = function() {
|
|
this._onTimeout = null;
|
|
if (this._handle) {
|
|
this._handle[kOnTimeout] = null;
|
|
this._handle.close();
|
|
} else {
|
|
exports.unenroll(this);
|
|
}
|
|
};
|
|
|
|
|
|
var immediateQueue = {};
|
|
L.init(immediateQueue);
|
|
|
|
|
|
function processImmediate() {
|
|
var queue = immediateQueue;
|
|
var domain, immediate;
|
|
|
|
immediateQueue = {};
|
|
L.init(immediateQueue);
|
|
|
|
while (L.isEmpty(queue) === false) {
|
|
immediate = L.shift(queue);
|
|
domain = immediate.domain;
|
|
|
|
if (domain)
|
|
domain.enter();
|
|
|
|
var threw = true;
|
|
try {
|
|
immediate._onImmediate();
|
|
threw = false;
|
|
} finally {
|
|
if (threw) {
|
|
if (!L.isEmpty(queue)) {
|
|
// Handle any remaining on next tick, assuming we're still
|
|
// alive to do so.
|
|
while (!L.isEmpty(immediateQueue)) {
|
|
L.append(queue, L.shift(immediateQueue));
|
|
}
|
|
immediateQueue = queue;
|
|
process.nextTick(processImmediate);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (domain)
|
|
domain.exit();
|
|
}
|
|
|
|
// Only round-trip to C++ land if we have to. Calling clearImmediate() on an
|
|
// immediate that's in |queue| is okay. Worst case is we make a superfluous
|
|
// call to NeedImmediateCallbackSetter().
|
|
if (L.isEmpty(immediateQueue)) {
|
|
process._needImmediateCallback = false;
|
|
}
|
|
}
|
|
|
|
|
|
function Immediate() { }
|
|
|
|
Immediate.prototype.domain = undefined;
|
|
Immediate.prototype._onImmediate = undefined;
|
|
Immediate.prototype._idleNext = undefined;
|
|
Immediate.prototype._idlePrev = undefined;
|
|
|
|
|
|
exports.setImmediate = function(callback, arg1, arg2, arg3) {
|
|
var i, args;
|
|
var len = arguments.length;
|
|
var immediate = new Immediate();
|
|
|
|
L.init(immediate);
|
|
|
|
switch (len) {
|
|
// fast cases
|
|
case 0:
|
|
case 1:
|
|
immediate._onImmediate = callback;
|
|
break;
|
|
case 2:
|
|
immediate._onImmediate = function() {
|
|
callback.call(immediate, arg1);
|
|
};
|
|
break;
|
|
case 3:
|
|
immediate._onImmediate = function() {
|
|
callback.call(immediate, arg1, arg2);
|
|
};
|
|
break;
|
|
case 4:
|
|
immediate._onImmediate = function() {
|
|
callback.call(immediate, arg1, arg2, arg3);
|
|
};
|
|
break;
|
|
// slow case
|
|
default:
|
|
args = new Array(len - 1);
|
|
for (i = 1; i < len; i++)
|
|
args[i - 1] = arguments[i];
|
|
|
|
immediate._onImmediate = function() {
|
|
callback.apply(immediate, args);
|
|
};
|
|
break;
|
|
}
|
|
|
|
if (!process._needImmediateCallback) {
|
|
process._needImmediateCallback = true;
|
|
process._immediateCallback = processImmediate;
|
|
}
|
|
|
|
if (process.domain)
|
|
immediate.domain = process.domain;
|
|
|
|
L.append(immediateQueue, immediate);
|
|
|
|
return immediate;
|
|
};
|
|
|
|
|
|
exports.clearImmediate = function(immediate) {
|
|
if (!immediate) return;
|
|
|
|
immediate._onImmediate = undefined;
|
|
|
|
L.remove(immediate);
|
|
|
|
if (L.isEmpty(immediateQueue)) {
|
|
process._needImmediateCallback = false;
|
|
}
|
|
};
|
|
|
|
|
|
// Internal APIs that need timeouts should use timers._unrefActive instead of
|
|
// timers.active as internal timeouts shouldn't hold the loop open
|
|
|
|
var unrefList, unrefTimer;
|
|
|
|
|
|
function unrefTimeout() {
|
|
var now = Timer.now();
|
|
|
|
debug('unrefTimer fired');
|
|
|
|
var diff, domain, first, threw;
|
|
while (first = L.peek(unrefList)) {
|
|
diff = now - first._idleStart;
|
|
|
|
if (diff < first._idleTimeout) {
|
|
diff = first._idleTimeout - diff;
|
|
unrefTimer.start(diff, 0);
|
|
unrefTimer.when = now + diff;
|
|
debug('unrefTimer rescheudling for later');
|
|
return;
|
|
}
|
|
|
|
L.remove(first);
|
|
|
|
domain = first.domain;
|
|
|
|
if (!first._onTimeout) continue;
|
|
if (domain && domain._disposed) continue;
|
|
|
|
try {
|
|
if (domain) domain.enter();
|
|
threw = true;
|
|
debug('unreftimer firing timeout');
|
|
first._called = true;
|
|
first._onTimeout();
|
|
threw = false;
|
|
if (domain)
|
|
domain.exit();
|
|
} finally {
|
|
if (threw) process.nextTick(unrefTimeout);
|
|
}
|
|
}
|
|
|
|
debug('unrefList is empty');
|
|
unrefTimer.when = -1;
|
|
}
|
|
|
|
|
|
exports._unrefActive = function(item) {
|
|
var msecs = item._idleTimeout;
|
|
if (!msecs || msecs < 0) return;
|
|
assert(msecs >= 0);
|
|
|
|
L.remove(item);
|
|
|
|
if (!unrefList) {
|
|
debug('unrefList initialized');
|
|
unrefList = {};
|
|
L.init(unrefList);
|
|
|
|
debug('unrefTimer initialized');
|
|
unrefTimer = new Timer();
|
|
unrefTimer.unref();
|
|
unrefTimer.when = -1;
|
|
unrefTimer[kOnTimeout] = unrefTimeout;
|
|
}
|
|
|
|
var now = Timer.now();
|
|
item._idleStart = now;
|
|
|
|
if (L.isEmpty(unrefList)) {
|
|
debug('unrefList empty');
|
|
L.append(unrefList, item);
|
|
|
|
unrefTimer.start(msecs, 0);
|
|
unrefTimer.when = now + msecs;
|
|
debug('unrefTimer scheduled');
|
|
return;
|
|
}
|
|
|
|
var when = now + msecs;
|
|
|
|
debug('unrefList find where we can insert');
|
|
|
|
var cur, them;
|
|
|
|
for (cur = unrefList._idlePrev; cur != unrefList; cur = cur._idlePrev) {
|
|
them = cur._idleStart + cur._idleTimeout;
|
|
|
|
if (when < them) {
|
|
debug('unrefList inserting into middle of list');
|
|
|
|
L.append(cur, item);
|
|
|
|
if (unrefTimer.when > when) {
|
|
debug('unrefTimer is scheduled to fire too late, reschedule');
|
|
unrefTimer.start(msecs, 0);
|
|
unrefTimer.when = when;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
debug('unrefList append to end');
|
|
L.append(unrefList, item);
|
|
};
|