Skip to content

Commit

Permalink
feat: add options.allowWarmUp as a creation option (#218)
Browse files Browse the repository at this point in the history
This feature allows the user to let the circuit warm up
before opening, even if every request is a failure or timeout.
The warmup duration is the value provided for
options.rollingCountTimeout.

Fixes: #217
  • Loading branch information
lance authored Aug 2, 2018
1 parent 791414a commit ff42d1b
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 4 deletions.
24 changes: 24 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ const defaults = {
* opening. Default 10.
* @param options.resetTimeout The time in milliseconds to wait before setting
* the breaker to `halfOpen` state, and trying the action again.
* @param options.rollingCountTimeout Sets the duration of the statistical rolling
* window, in milliseconds. This is how long Opossum keeps metrics for the circuit
* breaker to use and for publishing. Default: 10000
* @param options.rollingCountBuckets Sets the number of buckets the rolling
* statistical window is divided into. So, if options.rollingCountTimeout is
* 10000, and options.rollingCountBuckets is 10, then the statistical window will
* be 1000 1 second snapshots in the statistical window. Default: 10
* @param options.name the circuit name to use when reporting stats
* @param options.rollingPercentilesEnabled This property indicates whether
* execution latencies should be tracked and calculated as percentiles. If they
* are disabled, all summary statistics (mean, percentiles) are returned as -1.
* Default: false
* @param options.capacity the number of concurrent requests allowed. If the number
* currently executing function calls is equal to options.capacity, further calls
* to `fire()` are rejected until at least one of the current requests completes.
* @param options.errorThresholdPercentage the error percentage at which to open the
* circuit and start short-circuiting requests to fallback.
* @param options.enabled whether this circuit is enabled upon construction. Default: true
* @param options.allowWarmUp {boolean} determines whether to allow failures
* without opening the circuit during a brief warmup period (this is the
* `rollingCountDuration` property). Default: false
* 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
* @return a {@link CircuitBreaker} instance
*/
function circuitBreaker (action, options) {
Expand Down
25 changes: 23 additions & 2 deletions lib/circuit.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const GROUP = Symbol('group');
const HYSTRIX_STATS = Symbol('hystrix-stats');
const CACHE = new WeakMap();
const ENABLED = Symbol('Enabled');
const WARMING_UP = Symbol('warming-up');
const deprecation = `options.maxFailures is deprecated. \
Please use options.errorThresholdPercentage`;

Expand Down Expand Up @@ -58,6 +59,12 @@ Please use options.errorThresholdPercentage`;
* 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
* @param options.allowWarmUp {boolean} determines whether to allow failures
* without opening the circuit during a brief warmup period (this is the
* `rollingCountDuration` property). Default: false
* 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
*/
class CircuitBreaker extends EventEmitter {
constructor (action, options) {
Expand All @@ -67,10 +74,11 @@ class CircuitBreaker extends EventEmitter {
this.options.rollingCountBuckets = options.rollingCountBuckets || 10;
this.options.rollingPercentilesEnabled =
options.rollingPercentilesEnabled !== false;
this.options.capacity =
typeof options.capacity === 'number' ? options.capacity : 10;
this.options.capacity = Number.isInteger(options.capacity) ? options.capacity : 10;

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

this[WARMING_UP] = options.allowWarmUp === true;
this[STATUS] = new Status(this.options);
this[STATE] = CLOSED;
this[FALLBACK_FUNCTION] = null;
Expand All @@ -79,6 +87,14 @@ class CircuitBreaker extends EventEmitter {
this[GROUP] = options.group || this[NAME];
this[ENABLED] = options.enabled !== false;

if (this[WARMING_UP]) {
const timer = setTimeout(_ => (this[WARMING_UP] = false),
this.options.rollingCountTimeout);
if (typeof timer.unref === 'function') {
timer.unref();
}
}

if (typeof action !== 'function') {
this.action = _ => Promise.resolve(action);
} else this.action = action;
Expand Down Expand Up @@ -226,6 +242,10 @@ class CircuitBreaker extends EventEmitter {
return this[ENABLED];
}

get warmUp () {
return this[WARMING_UP];
}

/**
* 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 @@ -487,6 +507,7 @@ function fail (circuit, err, args, latency) {
* @event CircuitBreaker#failure
*/
circuit.emit('failure', err, latency);
if (circuit.warmUp) return;

// check stats to see if the circuit should be opened
const stats = circuit.stats;
Expand Down
3 changes: 1 addition & 2 deletions lib/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ class Status extends EventEmitter {
*/
get stats () {
const totals = this[WINDOW].reduce((acc, val) => {
// the window starts with all but one bucket undefined
if (!val) return acc;
if (!val) { return acc; }
Object.keys(acc).forEach(key => {
if (key !== 'latencyTimes' && key !== 'percentiles') {
(acc[key] += val[key] || 0);
Expand Down
64 changes: 64 additions & 0 deletions test/warmup-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

const test = require('tape');
const opossum = require('../');
const { passFail } = require('./common');

test('By default does not allow for warmup', 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('Allows for warmup when option is provided', t => {
t.plan(3);
const options = {
errorThresholdPercentage: 1,
resetTimeout: 100,
allowWarmUp: true
};

const breaker = opossum(passFail, options);
breaker.fire(-1)
.catch(e => t.equals(e, 'Error: -1 is < 0'))
.then(() => {
t.notOk(breaker.opened, 'should not be open after initial fire');
t.notOk(breaker.pendingClose,
'should not be pending close after initial fire');
});
});

test('Only warms up for rollingCountTimeout', t => {
t.plan(4);
const options = {
errorThresholdPercentage: 1,
resetTimeout: 100,
allowWarmUp: true,
rollingCountTimeout: 500
};

const breaker = opossum(passFail, options);
breaker.fire(-1)
.catch(e => t.equals(e, 'Error: -1 is < 0'))
.then(() => {
t.notOk(breaker.opened, 'should not be open after initial fire');
t.notOk(breaker.pendingClose,
'should not be pending close after initial fire');
})
.then(() => {
setTimeout(_ => {
t.notOk(breaker.warmUp, 'Warmup should end after rollingCountTimeout');
}, 500);
});
});

0 comments on commit ff42d1b

Please sign in to comment.