Skip to content

Commit 016eba5

Browse files
authored
feat: add enable/disable for a circuit (#160)
This commit also includes some code climate refactoring. Fixes: #136
1 parent 47b31d9 commit 016eba5

File tree

4 files changed

+175
-0
lines changed

4 files changed

+175
-0
lines changed

lib/circuit-helpers.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
const { FALLBACK_FUNCTION } = require('./symbols');
4+
5+
function handleError (error, circuit, timeout, args, latency, resolve, reject) {
6+
clearTimeout(timeout);
7+
fail(circuit, error, args, latency);
8+
const fb = fallback(circuit, error, args, latency);
9+
if (fb) resolve(fb);
10+
else reject(error);
11+
}
12+
13+
function fallback (circuit, err, args) {
14+
if (circuit[FALLBACK_FUNCTION]) {
15+
const result =
16+
circuit[FALLBACK_FUNCTION].apply(circuit[FALLBACK_FUNCTION], args);
17+
/**
18+
* Emitted when the circuit breaker executes a fallback function
19+
* @event CircuitBreaker#fallback
20+
*/
21+
circuit.emit('fallback', result, err);
22+
if (result instanceof Promise) return result;
23+
return Promise.resolve(result);
24+
}
25+
}
26+
27+
function fail (circuit, err, args, latency) {
28+
/**
29+
* Emitted when the circuit breaker action fails
30+
* @event CircuitBreaker#failure
31+
*/
32+
circuit.emit('failure', err, latency);
33+
34+
// check stats to see if the circuit should be opened
35+
const stats = circuit.stats;
36+
const errorRate = stats.failures / stats.fires * 100;
37+
if (errorRate > circuit.options.errorThresholdPercentage ||
38+
circuit.options.maxFailures >= stats.failures ||
39+
circuit.halfOpen) {
40+
circuit.open();
41+
}
42+
}
43+
44+
// http://stackoverflow.com/a/2117523
45+
const nextName = () =>
46+
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
47+
const r = Math.random() * 16 | 0;
48+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
49+
return v.toString(16);
50+
});
51+
52+
module.exports = exports = {
53+
handleError,
54+
fallback,
55+
fail,
56+
nextName
57+
};

lib/circuit.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const NAME = Symbol('name');
1616
const GROUP = Symbol('group');
1717
const HYSTRIX_STATS = Symbol('hystrix-stats');
1818
const CACHE = new WeakMap();
19+
const ENABLED = Symbol('Enabled');
1920
const deprecation = `options.maxFailures is deprecated. \
2021
Please use options.errorThresholdPercentage`;
2122

@@ -54,6 +55,8 @@ Please use options.errorThresholdPercentage`;
5455
* of the current requests completes.
5556
* @param options.errorThresholdPercentage {Number} the error percentage at
5657
* which to open the circuit and start short-circuiting requests to fallback.
58+
* @param options.enabled {boolean} whether this circuit is enabled upon
59+
* construction. Default: true
5760
*/
5861
class CircuitBreaker extends EventEmitter {
5962
constructor (action, options) {
@@ -73,6 +76,7 @@ class CircuitBreaker extends EventEmitter {
7376
this[PENDING_CLOSE] = false;
7477
this[NAME] = options.name || action.name || nextName();
7578
this[GROUP] = options.group || this[NAME];
79+
this[ENABLED] = options.enabled !== false;
7680

7781
if (typeof action !== 'function') {
7882
this.action = _ => Promise.resolve(action);
@@ -216,6 +220,11 @@ class CircuitBreaker extends EventEmitter {
216220
get hystrixStats () {
217221
return this[HYSTRIX_STATS];
218222
}
223+
224+
get enabled () {
225+
return this[ENABLED];
226+
}
227+
219228
/**
220229
* Provide a fallback function for this {@link CircuitBreaker}. This
221230
* function will be executed when the circuit is `fire`d and fails.
@@ -279,6 +288,16 @@ class CircuitBreaker extends EventEmitter {
279288
this.emit('cacheMiss');
280289
}
281290

291+
/**
292+
* https://github.com/bucharest-gold/opossum/issues/136
293+
*/
294+
if (!this[ENABLED]) {
295+
const result = this.action.apply(this.action, args);
296+
return (typeof result.then === 'function')
297+
? result
298+
: Promise.resolve(result);
299+
}
300+
282301
if (!this.closed && !this.pendingClose) {
283302
/**
284303
* Emitted when the circuit breaker is open and failing fast
@@ -416,6 +435,23 @@ class CircuitBreaker extends EventEmitter {
416435

417436
check();
418437
}
438+
439+
/**
440+
* Enables this circuit. If the circuit is the disabled
441+
* state, it will be reenabled. If not, this is essentially
442+
* a noop.
443+
*/
444+
enable () {
445+
this[ENABLED] = true;
446+
}
447+
448+
/**
449+
* Disables this circuit, causing all calls to the circuit's function
450+
* to be executed without circuit or fallback protection.
451+
*/
452+
disable () {
453+
this[ENABLED] = false;
454+
}
419455
}
420456

421457
function handleError (error, circuit, timeout, args, latency, resolve, reject) {

lib/symbols.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
3+
const STATE = Symbol('state');
4+
const OPEN = Symbol('open');
5+
const CLOSED = Symbol('closed');
6+
const HALF_OPEN = Symbol('half-open');
7+
const PENDING_CLOSE = Symbol('pending-close');
8+
const FALLBACK_FUNCTION = Symbol('fallback');
9+
const STATUS = Symbol('status');
10+
const NAME = Symbol('name');
11+
const GROUP = Symbol('group');
12+
const HYSTRIX_STATS = Symbol('hystrix-stats');
13+
const ENABLED = Symbol('Enabled');
14+
15+
module.exports = exports = {
16+
STATE,
17+
OPEN,
18+
CLOSED,
19+
HALF_OPEN,
20+
PENDING_CLOSE,
21+
FALLBACK_FUNCTION,
22+
STATUS,
23+
NAME,
24+
GROUP,
25+
HYSTRIX_STATS,
26+
ENABLED
27+
};

test/enable-disable-test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
const test = require('tape');
4+
const opossum = require('../');
5+
const { passFail } = require('./common');
6+
7+
test('Defaults to enabled', t => {
8+
t.plan(1);
9+
const breaker = opossum(passFail);
10+
t.equals(breaker.enabled, true);
11+
t.end();
12+
});
13+
14+
test('Accepts options.enabled', t => {
15+
t.plan(1);
16+
const breaker = opossum(passFail, { enabled: false });
17+
t.equals(breaker.enabled, false);
18+
t.end();
19+
});
20+
21+
test('When disabled the circuit should always be closed', t => {
22+
t.plan(8);
23+
const options = {
24+
errorThresholdPercentage: 1,
25+
resetTimeout: 1000
26+
};
27+
28+
const breaker = opossum(passFail, options);
29+
breaker.disable();
30+
breaker.fire(-1)
31+
.catch(e => t.equals(e, 'Error: -1 is < 0'))
32+
.then(() => {
33+
t.ok(breaker.closed, 'should be closed');
34+
t.notOk(breaker.pendingClose,
35+
'should not be pending close');
36+
})
37+
.then(() => {
38+
breaker // fire and fail again
39+
.fire(-1)
40+
.catch(e => t.equals(e, 'Error: -1 is < 0'))
41+
.then(() => {
42+
t.ok(breaker.closed, 'should be closed');
43+
t.notOk(breaker.pendingClose,
44+
'should not be pending close');
45+
});
46+
})
47+
.then(() => { // reenable the circuit
48+
breaker.enable();
49+
breaker.fire(-1)
50+
.catch(e => t.equals(e, 'Error: -1 is < 0'))
51+
.then(() => {
52+
t.ok(breaker.opened, 'should be closed');
53+
});
54+
});
55+
});

0 commit comments

Comments
 (0)