UNPKG

brakes

Version:

Node.js Circuit Breaker Pattern

133 lines (112 loc) 4.14 kB
'use strict'; const EventEmitter = require('events').EventEmitter; const Promise = require('bluebird'); const promisifyIfFunction = require('./utils').promisifyIfFunction; const TimeOutError = require('./TimeOutError'); const CircuitBrokenError = require('../lib/CircuitBrokenError'); const consts = require('./consts'); const defaultOptions = { isFailure: () => true }; /** * Class that can sit on top of a Brakes. It's basically just a pair of primary and fallback Promises you can put on * top of a Brake that monitors a common Service (eg: ). */ class Circuit extends EventEmitter { constructor(brakes, main, fallback, options) { super(); if (!(brakes instanceof EventEmitter)) { throw new Error(consts.NO_BRAKES); } this._brakes = brakes; if (!main || typeof main !== 'function') { throw new Error(consts.NO_FUNCTION); } else if (fallback) { if (typeof fallback !== 'function') { if (options) { throw new Error(consts.NO_FUNCTION); } options = fallback; fallback = undefined; } } this._opts = Object.assign({}, defaultOptions, options); this._this = this._opts.this || this; this._serviceCall = promisifyIfFunction(main, this._opts.isPromise, this._opts.isFunction); if (fallback) { this.fallback(fallback, this._opts.isPromise, this._opts.isFunction); } } exec() { this._brakes.emit('exec'); // Save circuit generation to scope so we can compare it // to the current generation when a request fails. // This prevents failures from bleeding between circuit generations. const execGeneration = this._brakes._circuitGeneration; if (this._brakes._circuitOpen) { this._brakes._stats.shortCircuit(); if (this._fallback) { return this._fallback.apply(this, arguments); } else if (this._brakes._fallback) { return this._brakes._fallback.apply(this, arguments); } return Promise.reject(new CircuitBrokenError(this._brakes.name, this._brakes._stats._totals, this._brakes._opts.threshold)); } const startTime = Date.now(); // we use _execPromise() wrapper on the service call promise // to allow us to more easily hook in stats reporting return this._execPromise .apply(this, arguments) .tap(() => this._brakes.emit('success', Date.now() - startTime)) .catch(err => { const endTime = Date.now() - startTime; // trigger hook listeners if (err instanceof TimeOutError) { this._brakes.emit('timeout', endTime, err, execGeneration); } else if (this._opts.isFailure(err)) { this._brakes.emit('failure', endTime, err, execGeneration); } // if fallback exists, call it upon failure // there are no listeners or stats collection for // the fallback function. The function is fire-and-forget // as far as `Brakes` is concerned if (this._fallback) { return this._fallback.apply(this, arguments); } else if (this._brakes._fallback) { return this._brakes._fallback.apply(this, arguments); } if (err && err.message && this._brakes.name && this._brakes._opts.modifyError) { err.message = `[Breaker: ${this._brakes.name}] ${err.message}`; } return Promise.reject(err); }); } /* Execute main service call */ _execPromise() { return new Promise((resolve, reject) => { // start timeout timer const timeoutTimer = setTimeout(() => { reject(new TimeOutError(consts.TIMEOUT)); }, this._opts.timeout || this._brakes._opts.timeout); this._serviceCall.apply(this._this, arguments).then(result => { clearTimeout(timeoutTimer); resolve(result); }).catch(err => { clearTimeout(timeoutTimer); reject(err); }); timeoutTimer.unref(); }); } fallback(func, isPromise, isFunction) { this._fallback = promisifyIfFunction(func, isPromise, isFunction); return this._fallback; } } module.exports = Circuit;