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

implement isOurError() #376

Merged
merged 3 commits into from
Oct 18, 2019
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
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 @@ -518,8 +529,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 @@ -656,6 +666,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