Skip to content

Commit

Permalink
feat: add enable/disable for a circuit (#160)
Browse files Browse the repository at this point in the history
This commit also includes some code climate refactoring. 

Fixes: #136
  • Loading branch information
lance authored Mar 23, 2018
1 parent 47b31d9 commit 016eba5
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 0 deletions.
57 changes: 57 additions & 0 deletions lib/circuit-helpers.js
Original file line number Diff line number Diff line change
@@ -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
};
36 changes: 36 additions & 0 deletions lib/circuit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`;

Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions lib/symbols.js
Original file line number Diff line number Diff line change
@@ -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
};
55 changes: 55 additions & 0 deletions test/enable-disable-test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
});

0 comments on commit 016eba5

Please sign in to comment.