From f9a720e338fd47632aa5e606d43569275bce92a2 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Sun, 21 Oct 2018 12:03:12 -0300 Subject: [PATCH] feat: add options.volumeThreshold This option prevents the circuit from opening unless the number of requests during the statistical window exceeds this threshold. Fixes: https://github.com/bucharest-gold/opossum/issues/232 --- index.js | 5 +++ lib/circuit.js | 12 +++++++ test/volume-threshold-test.js | 60 +++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 test/volume-threshold-test.js diff --git a/index.js b/index.js index aa572caa..ce9ba5c2 100644 --- a/index.js +++ b/index.js @@ -46,6 +46,11 @@ const defaults = { * allow before enabling the circuit. This can help in situations where no matter * what your `errorThresholdPercentage` is, if the first execution times out or * fails, the circuit immediately opens. Default: 0 + * @param options.volumeThreshold {Number} the minimum number of requests within + * the rolling statistical window that must exist before the circuit breaker + * can open. This is similar to `options.allowWarmUp` in that no matter how many + * failures there are, if the number of requests within the statistical window + * does not exceed this threshold, the circuit will remain closed. Default: 0 * @return a {@link CircuitBreaker} instance */ function circuitBreaker (action, options) { diff --git a/lib/circuit.js b/lib/circuit.js index de977644..124fbe92 100644 --- a/lib/circuit.js +++ b/lib/circuit.js @@ -18,6 +18,7 @@ const HYSTRIX_STATS = Symbol('hystrix-stats'); const CACHE = new WeakMap(); const ENABLED = Symbol('Enabled'); const WARMING_UP = Symbol('warming-up'); +const VOLUME_THRESHOLD = Symbol('volume-threshold'); const deprecation = `options.maxFailures is deprecated. \ Please use options.errorThresholdPercentage`; @@ -65,6 +66,11 @@ Please use options.errorThresholdPercentage`; * allow before enabling the circuit. This can help in situations where no matter * what your `errorThresholdPercentage` is, if the first execution times out or * fails, the circuit immediately opens. Default: 0 + * @param options.volumeThreshold {Number} the minimum number of requests within + * the rolling statistical window that must exist before the circuit breaker + * can open. This is similar to `options.allowWarmUp` in that no matter how many + * failures there are, if the number of requests within the statistical window + * does not exceed this threshold, the circuit will remain closed. Default: 0 */ class CircuitBreaker extends EventEmitter { constructor (action, options) { @@ -78,6 +84,7 @@ class CircuitBreaker extends EventEmitter { this.semaphore = new Semaphore(this.options.capacity); + this[VOLUME_THRESHOLD] = Number.isInteger(options.volumeThreshold) ? options.volumeThreshold : 0; this[WARMING_UP] = options.allowWarmUp === true; this[STATUS] = new Status(this.options); this[STATE] = CLOSED; @@ -246,6 +253,10 @@ class CircuitBreaker extends EventEmitter { return this[WARMING_UP]; } + get volumeThreshold () { + return this[VOLUME_THRESHOLD]; + } + /** * Provide a fallback function for this {@link CircuitBreaker}. This * function will be executed when the circuit is `fire`d and fails. @@ -511,6 +522,7 @@ function fail (circuit, err, args, latency) { // check stats to see if the circuit should be opened const stats = circuit.stats; + if (stats.fires < circuit.volumeThreshold) return; const errorRate = stats.failures / stats.fires * 100; if (errorRate > circuit.options.errorThresholdPercentage || circuit.options.maxFailures >= stats.failures || diff --git a/test/volume-threshold-test.js b/test/volume-threshold-test.js new file mode 100644 index 00000000..119c8ff7 --- /dev/null +++ b/test/volume-threshold-test.js @@ -0,0 +1,60 @@ +'use strict'; + +const test = require('tape'); +const opossum = require('../'); +const { passFail } = require('./common'); + +test('By default does not have a volume threshold', t => { + t.plan(3); + const options = { + errorThresholdPercentage: 1, + resetTimeout: 100 + }; + + const breaker = opossum(passFail, options); + breaker.fire(-1) + .catch(e => t.equals(e, 'Error: -1 is < 0')) + .then(() => { + t.ok(breaker.opened, 'should be open after initial fire'); + t.notOk(breaker.pendingClose, + 'should not be pending close after initial fire'); + }); +}); + +test('Has a volume threshold before tripping when option is provided', t => { + t.plan(6); + const options = { + errorThresholdPercentage: 1, + resetTimeout: 100, + volumeThreshold: 3 + }; + + const breaker = opossum(passFail, options); + breaker.fire(-1) + .then(t.fail) + .catch(e => { + t.notOk(breaker.opened, + 'should not be open before volume threshold has been reached'); + t.notOk(breaker.pendingClose, + 'should not be pending close before volume threshold has been reached'); + }) + .then(_ => { + breaker.fire(-1) + .then(t.fail) + .catch(e => { + t.notOk(breaker.opened, + 'should not be open before volume threshold has been reached'); + t.notOk(breaker.pendingClose, + 'should not be pending close before volume threshold has been reached'); + }) + .then(_ => { + breaker.fire(-1) + .catch(e => { + t.equals(e, 'Error: -1 is < 0'); + t.ok(breaker.opened, + 'should be open after volume threshold has been reached'); + }) + .then(t.end); + }); + }); +});