Skip to content

Commit

Permalink
feat: implement isOurError() (#376)
Browse files Browse the repository at this point in the history
`CircuitBreaker#isOurError()` provides a way of determining if the rejection from `fire()` or `call()` is from the circuit breaker or the action.
  • Loading branch information
tjenkinson authored and lance committed Oct 18, 2019
1 parent cde55eb commit f6a3e3a
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 17 deletions.
37 changes: 27 additions & 10 deletions lib/circuit.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const CACHE = new WeakMap();
const ENABLED = Symbol('Enabled');
const WARMING_UP = Symbol('warming-up');
const VOLUME_THRESHOLD = Symbol('volume-threshold');
const OUR_ERROR = Symbol('our-error');
const deprecation = `options.maxFailures is deprecated. \
Please use options.errorThresholdPercentage`;

Expand Down Expand Up @@ -96,6 +97,16 @@ Please use options.errorThresholdPercentage`;
* @fires CircuitBreaker#failure
*/
class CircuitBreaker extends EventEmitter {
/**
* Returns true if the provided error was generated here. It will be false
* if the error came from the action itself.
* @param {Error} error The Error to check.
* @returns {Boolean} true if the error was generated here
*/
static isOurError (error) {
return !!error[OUR_ERROR];
}

constructor (action, options = {}) {
super();
this.options = options;
Expand Down Expand Up @@ -363,7 +374,9 @@ class CircuitBreaker extends EventEmitter {
* function.
*
* @return {Promise<any>} promise resolves with the circuit function's return
* value on success or is rejected on failure of the action.
* value on success or is rejected on failure of the action. Use isOurError()
* to determine if a rejection was a result of the circuit breaker or the
* action.
*
* @fires CircuitBreaker#failure
* @fires CircuitBreaker#fallback
Expand Down Expand Up @@ -402,8 +415,7 @@ class CircuitBreaker extends EventEmitter {
*/
call (context, ...rest) {
if (this.isShutdown) {
const err = new Error('The circuit has been shutdown.');
err.code = 'ESHUTDOWN';
const err = buildError('The circuit has been shutdown.', 'ESHUTDOWN');
return Promise.reject(err);
}
const args = Array.prototype.slice.call(rest);
Expand Down Expand Up @@ -445,8 +457,7 @@ class CircuitBreaker extends EventEmitter {
* @event CircuitBreaker#reject
* @type {Error}
*/
const error = new Error('Breaker is open');
error.code = 'EOPENBREAKER';
const error = buildError('Breaker is open', 'EOPENBREAKER');

this.emit('reject', error);

Expand All @@ -464,9 +475,9 @@ class CircuitBreaker extends EventEmitter {
timeout = setTimeout(
() => {
timeoutError = true;
const error =
new Error(`Timed out after ${this.options.timeout}ms`);
error.code = 'ETIMEDOUT';
const error = buildError(
`Timed out after ${this.options.timeout}ms`, 'ETIMEDOUT'
);
/**
* Emitted when the circuit breaker action takes longer than
* `options.timeout`
Expand Down Expand Up @@ -517,8 +528,7 @@ class CircuitBreaker extends EventEmitter {
}
} else {
const latency = Date.now() - latencyStartTime;
const err = new Error('Semaphore locked');
err.code = 'ESEMLOCKED';
const err = buildError('Semaphore locked', 'ESEMLOCKED');
/**
* Emitted when the rate limit has been reached and there
* are no more locks to be obtained.
Expand Down Expand Up @@ -655,6 +665,13 @@ function fail (circuit, err, args, latency) {
}
}

function buildError (msg, code) {
const error = new Error(msg);
error.code = code;
error[OUR_ERROR] = true;
return error;
}

// http://stackoverflow.com/a/2117523
const nextName = () =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
Expand Down
5 changes: 4 additions & 1 deletion test/circuit-shutdown-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test('EventEmitter max listeners', t => {
});

test('Circuit shuts down properly', t => {
t.plan(5);
t.plan(6);
const breaker = new CircuitBreaker(passFail);
t.ok(breaker.fire(1), 'breaker is active');
breaker.shutdown();
Expand All @@ -28,6 +28,9 @@ test('Circuit shuts down properly', t => {
.catch(err => {
t.equals('ESHUTDOWN', err.code);
t.equals('The circuit has been shutdown.', err.message);
t.equals(
CircuitBreaker.isOurError(err), true, 'isOurError() should return true'
);
t.end();
});
});
33 changes: 27 additions & 6 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,23 @@ test('Using cache', t => {
});

test('Fails when the circuit function fails', t => {
t.plan(1);
t.plan(2);
const breaker = new CircuitBreaker(passFail);

breaker.fire(-1)
.then(() => t.fail)
.catch(e => {
t.equals(e, 'Error: -1 is < 0', 'expected error caught');
t.equals(
CircuitBreaker.isOurError(e), false, 'isOurError() should return false'
);
})
.then(_ => breaker.shutdown())
.then(t.end);
});

test('Fails when the circuit function times out', t => {
t.plan(2);
t.plan(3);
const expected = 'Timed out after 10ms';
const expectedCode = 'ETIMEDOUT';
const breaker = new CircuitBreaker(slowFunction, { timeout: 10 });
Expand All @@ -137,6 +140,9 @@ test('Fails when the circuit function times out', t => {
.catch(e => {
t.equals(e.message, expected, 'timeout message received');
t.equals(e.code, expectedCode, 'ETIMEDOUT');
t.equals(
CircuitBreaker.isOurError(e), true, 'isOurError() should return true'
);
})
.then(_ => breaker.shutdown())
.then(t.end);
Expand Down Expand Up @@ -189,7 +195,7 @@ test('Works with callback functions that fail', t => {
});

test('Breaker opens after a configurable number of failures', t => {
t.plan(2);
t.plan(3);
const breaker = new CircuitBreaker(passFail,
{ errorThresholdPercentage: 10 });

Expand All @@ -201,7 +207,14 @@ test('Breaker opens after a configurable number of failures', t => {
// with a valid value
breaker.fire(100)
.then(t.fail)
.catch(e => t.equals(e.message, 'Breaker is open', 'breaker opens'))
.catch(e => {
t.equals(e.message, 'Breaker is open', 'breaker opens');
t.equals(
CircuitBreaker.isOurError(e),
true,
'isOurError() should return true'
);
})
.then(_ => breaker.shutdown())
.then(t.end);
})
Expand Down Expand Up @@ -282,12 +295,17 @@ test('Executes fallback action, if one exists, when breaker is open', t => {
});

test('Passes error as last argument to the fallback function', t => {
t.plan(1);
t.plan(2);
const fails = -1;
const breaker = new CircuitBreaker(passFail, { errorThresholdPercentage: 1 });
breaker.on('fallback', result => {
t.equals(result,
`Error: ${fails} is < 0`, 'fallback received error as last parameter');
t.equals(
CircuitBreaker.isOurError(result),
false,
'isOurError() should return false'
);
breaker.shutdown();
t.end();
});
Expand Down Expand Up @@ -372,11 +390,14 @@ test('CircuitBreaker executes fallback when an action throws', t => {
});

test('CircuitBreaker emits failure when falling back', t => {
t.plan(2);
t.plan(3);
const breaker = new CircuitBreaker(passFail).fallback(() => 'fallback value');

breaker.on('failure', err => {
t.equals('Error: -1 is < 0', err, 'Expected failure');
t.equals(
CircuitBreaker.isOurError(err), false, 'isOurError() should return false'
);
});

breaker.fire(-1).then(result => {
Expand Down

0 comments on commit f6a3e3a

Please sign in to comment.