Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add options.allowWarmUp as a creation option #218

Merged
merged 1 commit into from
Aug 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});