diff --git a/lib/circuit-helpers.js b/lib/circuit-helpers.js new file mode 100644 index 00000000..c8a653a0 --- /dev/null +++ b/lib/circuit-helpers.js @@ -0,0 +1,57 @@ +'use strict'; + +const { FALLBACK_FUNCTION } = require('./symbols'); + +function handleError (error, circuit, timeout, args, latency, resolve, reject) { + clearTimeout(timeout); + fail(circuit, error, args, latency); + const fb = fallback(circuit, error, args, latency); + if (fb) resolve(fb); + else reject(error); +} + +function fallback (circuit, err, args) { + if (circuit[FALLBACK_FUNCTION]) { + const result = + circuit[FALLBACK_FUNCTION].apply(circuit[FALLBACK_FUNCTION], args); + /** + * Emitted when the circuit breaker executes a fallback function + * @event CircuitBreaker#fallback + */ + circuit.emit('fallback', result, err); + if (result instanceof Promise) return result; + return Promise.resolve(result); + } +} + +function fail (circuit, err, args, latency) { + /** + * Emitted when the circuit breaker action fails + * @event CircuitBreaker#failure + */ + circuit.emit('failure', err, latency); + + // check stats to see if the circuit should be opened + const stats = circuit.stats; + const errorRate = stats.failures / stats.fires * 100; + if (errorRate > circuit.options.errorThresholdPercentage || + circuit.options.maxFailures >= stats.failures || + circuit.halfOpen) { + circuit.open(); + } +} + +// http://stackoverflow.com/a/2117523 +const nextName = () => + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + +module.exports = exports = { + handleError, + fallback, + fail, + nextName +}; diff --git a/lib/circuit.js b/lib/circuit.js index 259cf33e..eb7ab817 100644 --- a/lib/circuit.js +++ b/lib/circuit.js @@ -16,6 +16,7 @@ const NAME = Symbol('name'); const GROUP = Symbol('group'); const HYSTRIX_STATS = Symbol('hystrix-stats'); const CACHE = new WeakMap(); +const ENABLED = Symbol('Enabled'); const deprecation = `options.maxFailures is deprecated. \ Please use options.errorThresholdPercentage`; @@ -54,6 +55,8 @@ Please use options.errorThresholdPercentage`; * of the current requests completes. * @param options.errorThresholdPercentage {Number} the error percentage at * which to open the circuit and start short-circuiting requests to fallback. + * @param options.enabled {boolean} whether this circuit is enabled upon + * construction. Default: true */ class CircuitBreaker extends EventEmitter { constructor (action, options) { @@ -73,6 +76,7 @@ class CircuitBreaker extends EventEmitter { this[PENDING_CLOSE] = false; this[NAME] = options.name || action.name || nextName(); this[GROUP] = options.group || this[NAME]; + this[ENABLED] = options.enabled !== false; if (typeof action !== 'function') { this.action = _ => Promise.resolve(action); @@ -216,6 +220,11 @@ class CircuitBreaker extends EventEmitter { get hystrixStats () { return this[HYSTRIX_STATS]; } + + get enabled () { + return this[ENABLED]; + } + /** * Provide a fallback function for this {@link CircuitBreaker}. This * function will be executed when the circuit is `fire`d and fails. @@ -279,6 +288,16 @@ class CircuitBreaker extends EventEmitter { this.emit('cacheMiss'); } + /** + * https://github.com/bucharest-gold/opossum/issues/136 + */ + if (!this[ENABLED]) { + const result = this.action.apply(this.action, args); + return (typeof result.then === 'function') + ? result + : Promise.resolve(result); + } + if (!this.closed && !this.pendingClose) { /** * Emitted when the circuit breaker is open and failing fast @@ -416,6 +435,23 @@ class CircuitBreaker extends EventEmitter { check(); } + + /** + * Enables this circuit. If the circuit is the disabled + * state, it will be reenabled. If not, this is essentially + * a noop. + */ + enable () { + this[ENABLED] = true; + } + + /** + * Disables this circuit, causing all calls to the circuit's function + * to be executed without circuit or fallback protection. + */ + disable () { + this[ENABLED] = false; + } } function handleError (error, circuit, timeout, args, latency, resolve, reject) { diff --git a/lib/symbols.js b/lib/symbols.js new file mode 100644 index 00000000..85ca2ea0 --- /dev/null +++ b/lib/symbols.js @@ -0,0 +1,27 @@ +'use strict'; + +const STATE = Symbol('state'); +const OPEN = Symbol('open'); +const CLOSED = Symbol('closed'); +const HALF_OPEN = Symbol('half-open'); +const PENDING_CLOSE = Symbol('pending-close'); +const FALLBACK_FUNCTION = Symbol('fallback'); +const STATUS = Symbol('status'); +const NAME = Symbol('name'); +const GROUP = Symbol('group'); +const HYSTRIX_STATS = Symbol('hystrix-stats'); +const ENABLED = Symbol('Enabled'); + +module.exports = exports = { + STATE, + OPEN, + CLOSED, + HALF_OPEN, + PENDING_CLOSE, + FALLBACK_FUNCTION, + STATUS, + NAME, + GROUP, + HYSTRIX_STATS, + ENABLED +}; diff --git a/test/enable-disable-test.js b/test/enable-disable-test.js new file mode 100644 index 00000000..780a15e3 --- /dev/null +++ b/test/enable-disable-test.js @@ -0,0 +1,55 @@ +'use strict'; + +const test = require('tape'); +const opossum = require('../'); +const { passFail } = require('./common'); + +test('Defaults to enabled', t => { + t.plan(1); + const breaker = opossum(passFail); + t.equals(breaker.enabled, true); + t.end(); +}); + +test('Accepts options.enabled', t => { + t.plan(1); + const breaker = opossum(passFail, { enabled: false }); + t.equals(breaker.enabled, false); + t.end(); +}); + +test('When disabled the circuit should always be closed', t => { + t.plan(8); + const options = { + errorThresholdPercentage: 1, + resetTimeout: 1000 + }; + + const breaker = opossum(passFail, options); + breaker.disable(); + breaker.fire(-1) + .catch(e => t.equals(e, 'Error: -1 is < 0')) + .then(() => { + t.ok(breaker.closed, 'should be closed'); + t.notOk(breaker.pendingClose, + 'should not be pending close'); + }) + .then(() => { + breaker // fire and fail again + .fire(-1) + .catch(e => t.equals(e, 'Error: -1 is < 0')) + .then(() => { + t.ok(breaker.closed, 'should be closed'); + t.notOk(breaker.pendingClose, + 'should not be pending close'); + }); + }) + .then(() => { // reenable the circuit + breaker.enable(); + breaker.fire(-1) + .catch(e => t.equals(e, 'Error: -1 is < 0')) + .then(() => { + t.ok(breaker.opened, 'should be closed'); + }); + }); +});