brakes
Version:
Node.js Circuit Breaker Pattern
254 lines (221 loc) • 6.54 kB
JavaScript
'use strict';
const EventEmitter = require('events').EventEmitter;
const Promise = require('bluebird');
const Stats = require('./Stats');
const promisifyIfFunction = require('./utils').promisifyIfFunction;
const globalStats = require('./globalStats');
const consts = require('./consts');
const Circuit = require('./Circuit');
const defaultOptions = {
bucketSpan: 1000,
bucketNum: 60,
name: 'defaultBrake',
group: 'defaultBrakeGroup',
circuitDuration: 30000,
statInterval: 1200,
registerGlobal: true,
waitThreshold: 100,
threshold: 0.5,
timeout: 15000,
healthCheckInterval: 5000,
healthCheck: undefined,
fallback: undefined,
isFunction: false,
isPromise: false,
modifyError: true
};
class Brakes extends EventEmitter {
constructor(func, opts) {
super();
if (typeof func === 'object' && !opts) {
opts = func;
func = undefined;
}
this._circuitOpen = false;
this._resetTimer = undefined;
this._fallback = undefined;
this._opts = Object.assign({}, defaultOptions, opts);
this._stats = new Stats(opts);
this._circuitGeneration = 1;
this.name = this._opts.name;
this.group = this._opts.group;
this._attachListeners();
this._stats.startSnapshots();
// register with global stats collector
if (this._opts.registerGlobal) {
globalStats.register(this);
}
const isPromise = this._opts.isPromise;
const isFunction = this._opts.isFunction;
// check if health check is in options
if (this._opts.healthCheck) {
this.healthCheck(this._opts.healthCheck, isPromise, isFunction);
}
// create a main circuit
if (func) {
this._mainCircuit = new Circuit(this, func, opts);
}
// check if fallback is in options
if (this._opts.fallback) {
this.fallback(this._opts.fallback, isPromise, isFunction);
}
}
/* Static method to get access to global stats */
static getGlobalStats() {
return globalStats;
}
/* Instance method to get access to global stats */
getGlobalStats() {
return globalStats;
}
/*
Perform all logic to allow proper garbage collection
*/
destroy() {
globalStats.deregister(this);
// the line below won't be needed with Node6, it provides
// a method 'eventNames()'
const eventNames = Object.keys(this._events);
eventNames.forEach(event => {
this.removeAllListeners(event);
});
}
exec() {
if (this._mainCircuit) {
return this._mainCircuit.exec.apply(this._mainCircuit, arguments);
}
return Promise.reject(new Error(consts.NO_FUNCTION));
}
_close() {
this._circuitOpen = false;
this.emit('circuitClosed');
}
_open() {
if (this._circuitOpen) return;
this.emit('circuitOpen');
this._circuitOpen = true;
this._circuitGeneration++;
if (this._healthCheck) {
this._setHealthInterval();
}
else {
this._resetCircuitTimeout();
}
}
_setHealthInterval() {
if (this._healthInterval) return;
this._healthInterval = setInterval(() => {
if (this._circuitOpen) {
this._healthCheck().then(() => {
// it is possible that in the meantime, the circuit is already
// closed by the previous health check
if (this._circuitOpen) {
this._stats.reset();
this._close();
}
this._healthInterval = clearInterval(this._healthInterval);
}).catch(err => {
this.emit('healthCheckFailed', err);
});
}
else {
// the circuit is closed out of health check,
// or from one of the cascading health checks
// (if the interval is not long enough to wait for one
// health check to complete, the previous health check might
// close the circuit) OR (manually closed).
this._healthInterval = clearInterval(this._healthInterval);
}
}, this._opts.healthCheckInterval);
this._healthInterval.unref();
}
_resetCircuitTimeout() {
const timer = setTimeout(() => {
this._stats.reset();
this._close();
}, this._opts.circuitDuration);
timer.unref();
}
/*
Allow user to pass a function to be used as a health check,
to close the circuit if the function succeeds.
*/
healthCheck(func, isPromise, isFunction) {
this._healthCheck = promisifyIfFunction(func, isPromise, isFunction);
}
/*
Allow user to pass function to be used as a fallback
*/
fallback(func, isPromise, isFunction) {
if (this._mainCircuit) {
this._fallback = this._mainCircuit.fallback(func, isPromise, isFunction);
}
else {
this._fallback = promisifyIfFunction(func, isPromise, isFunction);
}
}
/*
Listen to certain events and execute logic
This is mostly used for stats monitoring
*/
_attachListeners() {
this.on('success', d => {
this._successHandler(d);
});
this.on('timeout', (d, error, execGeneration) => {
this._timeoutHandler(d, execGeneration);
});
this.on('failure', (d, error, execGeneration) => {
this._failureHandler(d, execGeneration);
});
this._stats.on('update', d => {
this._checkStats(d);
});
this._stats.on('snapshot', d => {
this._snapshotHandler(d);
});
}
/*
Calculate stats and set internal state based on threshold
*/
_checkStats(stats) {
const pastThreshold = (stats.total || 0) > this._opts.waitThreshold;
if (!pastThreshold || !stats.total || this._circuitOpen) return;
if ((stats.successful / stats.total) < this._opts.threshold) {
this._open();
}
}
isOpen() {
return this._circuitOpen;
}
_snapshotHandler(stats) {
// attach stats metaData for easier downstream consumption
this.emit('snapshot', {
name: this.name,
group: this.group,
time: Date.now(),
open: this._circuitOpen,
circuitDuration: this._opts.circuitDuration,
threshold: this._opts.threshold,
waitThreshold: this._opts.waitThreshold,
stats
});
}
_successHandler(runTime) {
this._stats.success(runTime);
}
_timeoutHandler(runTime, execGeneration) {
if (execGeneration === this._circuitGeneration) {
this._stats.timeout(runTime);
}
}
_failureHandler(runTime, execGeneration) {
if (execGeneration === this._circuitGeneration) {
this._stats.failure(runTime);
}
}
subCircuit(service, fallback, options) {
return new Circuit(this, service, fallback, options);
}
}
module.exports = Brakes;