Skip to content

Commit f9a720e

Browse files
committed
feat: add options.volumeThreshold
This option prevents the circuit from opening unless the number of requests during the statistical window exceeds this threshold. Fixes: #232
1 parent 64952fd commit f9a720e

File tree

3 files changed

+77
-0
lines changed

3 files changed

+77
-0
lines changed

index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ const defaults = {
4646
* allow before enabling the circuit. This can help in situations where no matter
4747
* what your `errorThresholdPercentage` is, if the first execution times out or
4848
* fails, the circuit immediately opens. Default: 0
49+
* @param options.volumeThreshold {Number} the minimum number of requests within
50+
* the rolling statistical window that must exist before the circuit breaker
51+
* can open. This is similar to `options.allowWarmUp` in that no matter how many
52+
* failures there are, if the number of requests within the statistical window
53+
* does not exceed this threshold, the circuit will remain closed. Default: 0
4954
* @return a {@link CircuitBreaker} instance
5055
*/
5156
function circuitBreaker (action, options) {

lib/circuit.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const HYSTRIX_STATS = Symbol('hystrix-stats');
1818
const CACHE = new WeakMap();
1919
const ENABLED = Symbol('Enabled');
2020
const WARMING_UP = Symbol('warming-up');
21+
const VOLUME_THRESHOLD = Symbol('volume-threshold');
2122
const deprecation = `options.maxFailures is deprecated. \
2223
Please use options.errorThresholdPercentage`;
2324

@@ -65,6 +66,11 @@ Please use options.errorThresholdPercentage`;
6566
* allow before enabling the circuit. This can help in situations where no matter
6667
* what your `errorThresholdPercentage` is, if the first execution times out or
6768
* fails, the circuit immediately opens. Default: 0
69+
* @param options.volumeThreshold {Number} the minimum number of requests within
70+
* the rolling statistical window that must exist before the circuit breaker
71+
* can open. This is similar to `options.allowWarmUp` in that no matter how many
72+
* failures there are, if the number of requests within the statistical window
73+
* does not exceed this threshold, the circuit will remain closed. Default: 0
6874
*/
6975
class CircuitBreaker extends EventEmitter {
7076
constructor (action, options) {
@@ -78,6 +84,7 @@ class CircuitBreaker extends EventEmitter {
7884

7985
this.semaphore = new Semaphore(this.options.capacity);
8086

87+
this[VOLUME_THRESHOLD] = Number.isInteger(options.volumeThreshold) ? options.volumeThreshold : 0;
8188
this[WARMING_UP] = options.allowWarmUp === true;
8289
this[STATUS] = new Status(this.options);
8390
this[STATE] = CLOSED;
@@ -246,6 +253,10 @@ class CircuitBreaker extends EventEmitter {
246253
return this[WARMING_UP];
247254
}
248255

256+
get volumeThreshold () {
257+
return this[VOLUME_THRESHOLD];
258+
}
259+
249260
/**
250261
* Provide a fallback function for this {@link CircuitBreaker}. This
251262
* function will be executed when the circuit is `fire`d and fails.
@@ -511,6 +522,7 @@ function fail (circuit, err, args, latency) {
511522

512523
// check stats to see if the circuit should be opened
513524
const stats = circuit.stats;
525+
if (stats.fires < circuit.volumeThreshold) return;
514526
const errorRate = stats.failures / stats.fires * 100;
515527
if (errorRate > circuit.options.errorThresholdPercentage ||
516528
circuit.options.maxFailures >= stats.failures ||

test/volume-threshold-test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
const test = require('tape');
4+
const opossum = require('../');
5+
const { passFail } = require('./common');
6+
7+
test('By default does not have a volume threshold', t => {
8+
t.plan(3);
9+
const options = {
10+
errorThresholdPercentage: 1,
11+
resetTimeout: 100
12+
};
13+
14+
const breaker = opossum(passFail, options);
15+
breaker.fire(-1)
16+
.catch(e => t.equals(e, 'Error: -1 is < 0'))
17+
.then(() => {
18+
t.ok(breaker.opened, 'should be open after initial fire');
19+
t.notOk(breaker.pendingClose,
20+
'should not be pending close after initial fire');
21+
});
22+
});
23+
24+
test('Has a volume threshold before tripping when option is provided', t => {
25+
t.plan(6);
26+
const options = {
27+
errorThresholdPercentage: 1,
28+
resetTimeout: 100,
29+
volumeThreshold: 3
30+
};
31+
32+
const breaker = opossum(passFail, options);
33+
breaker.fire(-1)
34+
.then(t.fail)
35+
.catch(e => {
36+
t.notOk(breaker.opened,
37+
'should not be open before volume threshold has been reached');
38+
t.notOk(breaker.pendingClose,
39+
'should not be pending close before volume threshold has been reached');
40+
})
41+
.then(_ => {
42+
breaker.fire(-1)
43+
.then(t.fail)
44+
.catch(e => {
45+
t.notOk(breaker.opened,
46+
'should not be open before volume threshold has been reached');
47+
t.notOk(breaker.pendingClose,
48+
'should not be pending close before volume threshold has been reached');
49+
})
50+
.then(_ => {
51+
breaker.fire(-1)
52+
.catch(e => {
53+
t.equals(e, 'Error: -1 is < 0');
54+
t.ok(breaker.opened,
55+
'should be open after volume threshold has been reached');
56+
})
57+
.then(t.end);
58+
});
59+
});
60+
});

0 commit comments

Comments
 (0)