circuit-fuses
Version:
Wrapper around node-circuitbreaker to define a callback interface
244 lines (215 loc) • 8.12 kB
JavaScript
/* eslint-disable max-classes-per-file */
'use strict';
const circuitbreaker = require('screwdriver-node-circuitbreaker');
const logger = require('screwdriver-logger');
const retryFn = require('screwdriver-retry-function');
const EventEmitter = require('events');
/* Class representing a node circuit breaker.
* @extends EventEmitter
*/
class CircuitBreaker extends EventEmitter {
/**
* Construct a CircuitBreaker object
* @method constructor
* @param {Function} command The command to be running that requires the circuit breaker
* @param {Object} [options] Options to configure the circuit breaker with
* @param {Number} options.breaker.timeout The timeout in ms to wait for a command to complete
* @param {Number} options.breaker.maxFailures The number of failures before the circuit is tripped
* @param {Number} options.breaker.resetTimeout The timeout in ms to reset the switch
* @param {Number} options.retry.retries The number of retries to do before passing back a failure
* @param {Number} options.retry.factor The exponential factor to use
* @param {Number} options.retry.minTimeout The timeout to wait before doing the first retry
* @param {Number} options.retry.maxTimeout The max timeout to wait before retrying
* @param {Boolean} options.retry.randomize Randomize the timeout
*/
constructor(command, options) {
super();
const optionsToCompare = options || {};
const breakerOptions = optionsToCompare.breaker || {};
const retryOptions = optionsToCompare.retry || {};
this.command = command;
this.breakerOptions = {
timeout: breakerOptions.timeout || 10000,
maxFailures: breakerOptions.maxFailures || 5,
resetTimeout: breakerOptions.resetTimeout || 50,
errorFn: breakerOptions.errorFn || (() => true)
};
this.retryOptions = {
retries: retryOptions.retries || 5,
factor: retryOptions.factor || 2,
minTimeout: retryOptions.minTimeout || 1000,
maxTimeout: retryOptions.maxTimeout || Number.MAX_SAFE_INTEGER,
randomize: retryOptions.randomize || false
};
this.shouldRetry = (options && options.shouldRetry) || (() => true);
this.breaker = circuitbreaker(this.command, this.breakerOptions);
this.breaker.on('open', () => {
logger.error(`Breaker with function ${this.command.toString()} \
was tripped on ${new Date().toUTCString()}`);
this.emit('open');
});
}
/**
* Retry wrapper for the circuit breaker to retry on failure as long as circuit is closed
* @method runCommand
* @param {arguments} arguments List of arguments to call the circuit breaker command with
* @param {Function} [callback] Last argument is the callback to callback upon completion
*/
runCommand() {
const args = Array.prototype.slice.call(arguments);
let callback;
if (args.length >= 1 && typeof args[args.length - 1] === 'function') {
callback = args.pop();
}
const wrapBreaker = cb => {
this.breaker.apply(this.breaker, args).then(
data => cb(null, data),
err => cb(err)
);
};
const { shouldRetry } = this;
return new Promise((resolve, reject) => {
retryFn(
{
method: wrapBreaker,
context: this,
options: this.retryOptions,
shouldRetry: err => err && this.isClosed() && shouldRetry(err, args)
},
(err, ...data) => {
if (typeof callback === 'function') {
return callback(err, ...data);
}
if (err) {
const safeArgs = JSON.stringify(args, (key, value) => (key === 'token' ? '[REDACTED]' : value));
logger.error(`Getting errors with ${safeArgs}: ${err}`);
if (err.statusCode === undefined) {
if (err.message.indexOf('CircuitBreaker timeout') !== -1) {
err.statusCode = 504;
}
}
return reject(err);
}
return resolve(...data);
}
);
});
}
/**
* Return boolean whether the state of the breaker is closed
* @method isClosed()
* @returns {Boolean} Whether or not the breaker is closed
*/
isClosed() {
return this.breaker.isClosed();
}
/**
* Get the Total Number of requests
* @method getTotalRequests
* @returns {Number} Total number requests
*/
getTotalRequests() {
return this.breaker.stats.totalRequests;
}
/**
* Get the Total Number of request timeouts
* @method getTimeouts
* @returns {Number} Total number timeouts
*/
getTimeouts() {
return this.breaker.stats.timeouts;
}
/**
* Get the Total Number of successful requests
* @method getSuccessfulRequests
* @returns {Number} Total number successful requests
*/
getSuccessfulRequests() {
return this.breaker.stats.successfulResponses;
}
/**
* Get the Total Number of failed requests
* @method getSuccessfulRequests
* @returns {Number} Total number failed requests
*/
getFailedRequests() {
return this.breaker.stats.failedResponses;
}
/**
* Get the Total Number of concurrent requests
* @method getConcurrentRequests
* @returns {Number} Total number concurrent requests
*/
getConcurrentRequests() {
return this.breaker.stats.concurrentRequests();
}
/**
* Get the Average response time of the requests
* @method getAverageRequestTime
* @returns {Number} Average response time per request
*/
getAverageRequestTime() {
return this.breaker.stats.averageResponseTime();
}
/**
* Force the circuit breaker open
* @method forceOpen
*/
forceOpen() {
if (this.breaker.isClosed()) {
logger.info(`Forcing open ${this.command.toString()}`);
this.breaker.forceOpen();
}
}
/**
* Retrieve stats for the breaker
* @method stats
* @returns {Object} Object containing stats for the breaker
*/
stats() {
return {
requests: {
total: this.breaker.stats.totalRequests,
timeouts: this.breaker.stats.timeouts,
success: this.breaker.stats.successfulResponses,
failure: this.breaker.stats.failedResponses,
concurrent: this.breaker.stats.concurrentRequests(),
averageTime: this.breaker.stats.averageResponseTime()
},
breaker: {
isClosed: this.breaker.isClosed()
}
};
}
}
/* Class representing a collection of CircuitBreaker instances */
class FuseBox {
/**
* Construct a FuseBox object
* @constructor
*/
constructor() {
this.fuses = [];
}
/**
* Add an existing circuit breaker to the fuse box
* @method addFuse
* @param {CircuitBreaker} breaker The circuit breaker to be added to the fuse box
*/
addFuse(breaker) {
const self = this;
breaker.on('open', () => self.tripFuses());
this.fuses.push(breaker);
}
/**
* Trip all the circuit breakers inside the fuse box.
* @method tripFuses
*/
tripFuses() {
this.fuses.map(fuse => fuse.forceOpen());
}
}
module.exports = {
breaker: CircuitBreaker,
box: FuseBox
};