node-toobusy
Version:
Don't fall over when your Node.JS server is too busy. Now without native dependencies!
221 lines (190 loc) • 6.94 kB
JavaScript
;
var events = require('events');
//
// Constants
//
var STANDARD_HIGHWATER = 70;
var STANDARD_INTERVAL = 500;
var LAG_EVENT = "LAG_EVENT";
// A dampening factor. When determining average calls per second or
// current lag, we weigh the current value against the previous value 2:1
// to smooth spikes.
// See https://en.wikipedia.org/wiki/Exponential_smoothing
var SMOOTHING_FACTOR = 1/3;
//
// Vars
//
var lastTime = Date.now();
var highWater = STANDARD_HIGHWATER;
var interval = STANDARD_INTERVAL;
var smoothingFactorOnRise = SMOOTHING_FACTOR;
var smoothingFactorOnFall = 1 - SMOOTHING_FACTOR;
var currentLag = 0;
var checkInterval;
var lagEventThreshold = -1;
var eventEmitter = new events.EventEmitter();
/**
* Main export function.
* @return {Boolean} True if node process is too busy.
*/
var toobusy = function(){
// If current lag is < 2x the highwater mark, we don't always call it 'too busy'. E.g. with a 50ms lag
// and a 40ms highWater (1.25x highWater), 25% of the time we will block. With 80ms lag and a 40ms highWater,
// we will always block.
var pctToBlock = (currentLag - highWater) / highWater;
return Math.random() < pctToBlock;
};
/**
* Resets variables to their default values
* Affected variables: highWater, interval, smoothingFactorOnRise and smoothingFactorOnFall
*/
toobusy.reset = function() {
highWater = STANDARD_HIGHWATER;
interval = STANDARD_INTERVAL;
smoothingFactorOnRise = SMOOTHING_FACTOR;
smoothingFactorOnFall = 1 - SMOOTHING_FACTOR;
};
/**
* Sets or gets the current check interval.
* If you want more sensitive checking, set a faster (lower) interval. A lower maxLag can also create a more
* sensitive check.
* @param {Number} [newInterval] New interval to set. If not provided, will return the existing interval.
* @return {Number} New or existing interval.
*/
toobusy.interval = function(newInterval) {
if (!newInterval) return interval;
if (typeof newInterval !== "number") throw new Error("Interval must be a number.");
newInterval = Math.round(newInterval);
if(newInterval < 16) throw new Error("Interval should be greater than 16ms.");
toobusy.shutdown();
interval = newInterval;
start();
return interval;
};
/**
* Returns last lag reading from last check interval.
* @return {Number} Lag in ms.
*/
toobusy.lag = function(){
return Math.round(currentLag);
};
/**
* Set or get the current max latency threshold. Default is 70ms.
*
* Note that if event loop lag goes over this threshold, the process is not always 'too busy' - the farther
* it goes over the threshold, the more likely the process will be considered too busy.
*
* The percentage is equal to the percent over the max lag threshold. So 1.25x over the maxLag will indicate
* too busy 25% of the time. 2x over the maxLag threshold will indicate too busy 100% of the time.
* @param {Number} [newLag] New maxLag (highwater) threshold.
* @return {Number} New or existing maxLag (highwater) threshold.
*/
toobusy.maxLag = function(newLag){
if(!newLag) return highWater;
// If an arg was passed, try to set highWater.
if (typeof newLag !== "number") throw new Error("MaxLag must be a number.");
newLag = Math.round(newLag);
if(newLag < 10) throw new Error("Maximum lag should be greater than 10ms.");
highWater = newLag;
return highWater;
};
/**
* Private function used to set the two smoothing factors
* @param {number} newFactor
* @param {boolean} onFallSelector Optional parameter used to selected which factor to set
*/
function setSmoothingFactor(newFactor, onFallSelector){
if (typeof newFactor !== "number") throw new Error("NewFactor must be a number.");
if(newFactor <= 0 || newFactor > 1) throw new Error("Smoothing factor should be in range ]0,1].");
if (onFallSelector) {
smoothingFactorOnFall = newFactor;
} else {
smoothingFactorOnRise = newFactor;
}
};
/**
* Set or get the smoothing factor on rise. Default is 0.3333....
* This is the factor applied when the measured lag is higher than currentLag
*
* The smoothing factor per the standard exponential smoothing formula "αtn + (1-α)tn-1"
* See: https://en.wikipedia.org/wiki/Exponential_smoothing
*
* @param {Number} [newFactor] New smoothing factor.
* @return {Number} New or existing smoothing factor.
*/
toobusy.smoothingFactorOnRise = function(newFactor) {
if(!newFactor) return smoothingFactorOnRise;
setSmoothingFactor(newFactor);
return smoothingFactorOnRise;
};
/**
* Set or get the smoothing factor on fall. Default is 0.6666....
* This is the factor applied when the measured lag is lower than currentLag
*
* @param {Number} [newFactor] New smoothing factor.
* @return {Number} New or existing smoothing factor.
*/
toobusy.smoothingFactorOnFall = function(newFactor) {
if(!newFactor) return smoothingFactorOnFall;
setSmoothingFactor(newFactor, true);
return smoothingFactorOnFall;
};
/**
* Shuts down toobusy.
*
* Not necessary to call this manually, only do this if you know what you're doing. `unref()` is called
* on toobusy's check interval, so it will never keep the server open.
*/
toobusy.shutdown = function(){
currentLag = 0;
checkInterval = clearInterval(checkInterval);
};
toobusy.started = function() {
return !!checkInterval;
};
/**
* Registers an event listener for lag events,
* optionally specify a minimum value threshold for events being emitted
* @param {Function} fn Function of form onLag(value: number) => void
* @param {number} [threshold=maxLag] Optional minimum lag value for events to be emitted
*/
toobusy.onLag = function (fn, threshold) {
if (typeof threshold === "number") {
lagEventThreshold = threshold;
} else {
lagEventThreshold = toobusy.maxLag();
}
eventEmitter.on(LAG_EVENT, fn);
};
/**
* Calculates the new value that will be assigned to currentLag
* Overwrite this function if you need a different behavior
* @param {number} lag The last measured lag
* @param {number} cLag The currentLag value
* @param {number} sFactor The smoothingFactor value
* @return {number} The calculated value
*/
toobusy.lagFunction = function (lag, cLag, sFactorRise, sFactorFall) {
var factor = lag > cLag ? sFactorRise : sFactorFall;
return factor * lag + (1 - factor) * cLag;
}
/**
* Private - starts checking lag.
*/
function start() {
checkInterval = setInterval(function(){
var now = Date.now();
var lag = now - lastTime;
lag = Math.max(0, lag - interval);
currentLag = toobusy.lagFunction(lag, currentLag, smoothingFactorOnRise, smoothingFactorOnFall);
lastTime = now;
if (lagEventThreshold !== -1 && currentLag > lagEventThreshold) {
eventEmitter.emit(LAG_EVENT, currentLag);
}
}, interval);
// Don't keep process open just for this timer.
checkInterval.unref();
}
// Kickoff the checking!
start();
module.exports = toobusy;