Skip to content

Commit

Permalink
Merge pull request #16 from trygve-lie/refined-status-codes
Browse files Browse the repository at this point in the history
Refine which http status codes to trip on
  • Loading branch information
trygve-lie authored Nov 7, 2018
2 parents 89df292 + 23dde9d commit 54f4d8a
Show file tree
Hide file tree
Showing 18 changed files with 393 additions and 20 deletions.
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ An Object containing misc configuration. The following values can be provided:
* **maxFailures** - `Number` - Default number of failures which should occur before the breaker switch into open state. Can be overrided by each host. Default: 5.
* **maxAge** - `Number` - Default time in milliseconds from the breaker entered open state until it enters half open state. Can be overrided by each host. Default: 5000ms.
* **timeout** - `Number` - Default request timeout in milliseconds. Can be overrided by each host. Default: 20000ms.
* **onResponse** - `Function` - Default function execute on a http response to tripp the breaker. Default trips the breaker on http status codes: `408`, `413`, `429`, `500`, `502`, `503`, `504`.


## API
Expand All @@ -100,7 +101,8 @@ This method take the following arguments:
* **host** - `String` - The host to guard. Required.
* **options.maxFailures** - `Number` - Number of failures which should occur before the breaker switch into open state. Inherits from constructor if unset.
* **options.maxAge** - `Number` - Time in milliseconds from the breaker entered open state until it enters half open state. Inherits from constructor if unset.
* **options.timeout** - `Number` - Request timeout in milliseconds. Can be overrided by each host. Inherits from constructor if unset.
* **options.timeout** - `Number` - Request timeout in milliseconds. Inherits from constructor if unset.
* **options.onResponse** - `Function` - Function to be execute on a http response to tripp the breaker. Inherits from constructor if unset.

### .del(host)

Expand Down Expand Up @@ -195,6 +197,64 @@ breaker.on('half_open', (host) => {
```


## HTTP Responses

A struggeling downstream service can despite that respond to http requests. Due to this
Circuit-b does monitor the http statuses on requests and trip the breaker on sertain
status codes.

The breaker will trip when the following http status codes occure in a downstream service:

* `408` - [Request Timeout](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408)
* `413` - [Payload Too Large](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413)
* `429` - [Too Many Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429)
* `500` - [Internal Server Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500)
* `502` - [Bade Gateway](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502)
* `503` - [Service Unavailable](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503)
* `504` - [Gateway Timeout](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)


### Custom tripper on HTTP responses

It is possible to provide a custom tripper for HTTP responses by providing a function to
the `onResponse` options on the `constructor` (globally) or the `.set()` method (peer
host).

The function will be executed on each intercepted request with the [http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)
as the first argument.

The function must return a `Boolean` where `false` will **not** trip the breaker and `true`
will trip the breaker.

Example of tripping the breaker only when the downstream server responds with http status
code 500:

```js
const request = require('request');
const Breaker = require('circuit-b');

const breaker = new Breaker();

breaker.set('api.somewhere.com', {
onResponse: (res) => {
if (res.statusCode === 500) {
return true;
}
return false;
}
});

breaker.enable();

// Will choke on 500 errors, and trigger breaker
request('http://api.somewhere.com', (error, response, body) => {
console.log(body);
});
```

NOTE: Its adviced to **not** do any asyncronous operations in this method.


## Timeouts

One aspect of monitoring health status of a downstream service is timeouts of the http
Expand Down
13 changes: 11 additions & 2 deletions lib/breaker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@
const EventEmitter = require('events');
const Metrics = require('@metrics/client');
const assert = require('assert');
const response = require('./response');
const utils = require('./utils');

const stats = Symbol('_stats');

const Breaker = class Breaker extends EventEmitter {
constructor(host = '', { maxFailures = 5, maxAge = 5000, timeout = 20000 } = {}) {
constructor(host = '', {
maxFailures = 5, onResponse = response, maxAge = 5000, timeout = 20000,
} = {}) {
super();

assert(host, 'The argument "host" must be provided');
assert(Number.isInteger(maxFailures), `Provided value, ${maxFailures}, to argument "maxFailures" is not a number`);
assert(utils.isFunction(onResponse), 'Provided value to argument "onResponse" is not a function');
assert(Number.isInteger(timeout), `Provided value, ${timeout}, to argument "timeout" is not a number`);
assert(Number.isInteger(maxAge), `Provided value, ${maxAge}, to argument "maxAge" is not a number`);


Object.defineProperty(this, 'host', {
value: host,
enumerable: true,
Expand All @@ -26,6 +30,11 @@ const Breaker = class Breaker extends EventEmitter {
enumerable: true,
});

Object.defineProperty(this, 'onResponse', {
value: onResponse,
enumerable: true,
});

Object.defineProperty(this, 'maxAge', {
value: maxAge,
});
Expand Down
34 changes: 20 additions & 14 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@ const asyncHooks = require('async_hooks');
const Metrics = require('@metrics/client');
const assert = require('assert');
const abslog = require('abslog');
const response = require('./response');
const Breaker = require('./breaker');
const errors = require('./error');
const utils = require('./utils');

const hook = Symbol('_hook');

const CircuitB = class CircuitB extends EventEmitter {
constructor({
maxFailures = 5, maxAge = 5000, timeout = 20000, logger = undefined,
maxFailures = 5,
onResponse = response,
timeout = 20000,
maxAge = 5000,
logger = undefined,
} = {}) {
super();

assert(Number.isInteger(maxFailures), `Provided value, ${maxFailures}, to argument "maxFailures" is not a number`);
assert(utils.isFunction(onResponse), 'Provided value to argument "onResponse" is not a function');
assert(Number.isInteger(timeout), `Provided value, ${timeout}, to argument "timeout" is not a number`);
assert(Number.isInteger(maxAge), `Provided value, ${maxAge}, to argument "maxAge" is not a number`);

Expand All @@ -32,6 +39,10 @@ const CircuitB = class CircuitB extends EventEmitter {
value: timeout,
});

Object.defineProperty(this, 'onResponse', {
value: onResponse,
});

Object.defineProperty(this, 'maxFailures', {
value: maxFailures,
});
Expand Down Expand Up @@ -170,21 +181,13 @@ const CircuitB = class CircuitB extends EventEmitter {
return;
}

const code = httpMsg.res.statusCode;

if (code >= 400 && code <= 499) {
this.log.debug('Circuit breaker got "end" event on resource - http status is 4xx', breaker.host, asyncId);
tripped = breaker.trip();
return;
}

if (code >= 500 && code <= 599) {
this.log.debug('Circuit breaker got "end" event on resource - http status is 5xx', breaker.host, asyncId);
if (breaker.onResponse(httpMsg.res)) {
this.log.debug(`Circuit breaker got "end" event on resource - http status is ${httpMsg.res.statusCode}`, breaker.host, asyncId);
tripped = breaker.trip();
return;
}

this.log.debug('Circuit breaker got "end" event on resource - http status is 2xx', breaker.host, asyncId);
this.log.debug(`Circuit breaker got "end" event on resource - http status is ${httpMsg.res.statusCode}`, breaker.host, asyncId);
breaker.reset();
}
});
Expand All @@ -205,10 +208,13 @@ const CircuitB = class CircuitB extends EventEmitter {

set(host, {
maxFailures = this.maxFailures,
maxAge = this.maxAge,
onResponse = this.onResponse,
timeout = this.timeout,
maxAge = this.maxAge,
} = {}) {
const breaker = new Breaker(host, { maxFailures, maxAge, timeout });
const breaker = new Breaker(host, {
maxFailures, onResponse, maxAge, timeout,
});
breaker.on('open', (hostname) => {
this.emit('open', hostname);
});
Expand Down
43 changes: 43 additions & 0 deletions lib/response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

module.exports = (res = {}) => {
if (res.statusCode === undefined) {
return true;
}

let result = false;
switch (res.statusCode) {
case 408: // Request Timeout
result = true;
break;

case 413: // Payload Too Large
result = true;
break;

case 429: // Too Many Requests
result = true;
break;

case 500: // Internal Server Error
result = true;
break;

case 502: // Bad Gateway
result = true;
break;

case 503: // Service Unavailable
result = true;
break;

case 504: // Gateway Timeout
result = true;
break;

default:
result = false;
}

return result;
};
7 changes: 7 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

const isFunction = (fn) => {
const type = {}.toString.call(fn);
return type === '[object Function]' || type === '[object AsyncFunction]';
};
module.exports.isFunction = isFunction;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"test:axios": "tape test/integration-axios.js | tap-summary",
"test:http": "tape test/integration-http.js | tap-summary",
"test:got": "tape test/integration-got.js | tap-summary",
"test:response": "tape test/response.js | tap-summary",
"test:breaker": "tape test/breaker.js | tap-summary",
"test:utils": "tape test/utils.js | tap-summary",
"test:main": "tape test/main.js | tap-summary",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
Expand Down
16 changes: 16 additions & 0 deletions test/integration-axios.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const timeout = require('./integration/timeout');
const http400 = require('./integration/http-status-400');
const http500 = require('./integration/http-status-500');
const errorFlight = require('./integration/error-in-flight');
const customTripper = require('./integration/custom-tripper');

const client = options => new Promise((resolve) => {
axios.get(`http://${options.host}:${options.port}/`)
Expand Down Expand Up @@ -121,6 +122,21 @@ test('integration - axios - error', async (t) => {
t.end();
});

test('integration - axios - custom tripper', async (t) => {
const result = await customTripper(client);
t.deepEqual(result, [
'ok',
'ok',
'http error',
'http error',
'http error',
'http error',
'ok',
'ok',
]);
t.end();
});

test('after', async (t) => {
await after();
t.end();
Expand Down
16 changes: 16 additions & 0 deletions test/integration-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const timeout = require('./integration/timeout');
const http400 = require('./integration/http-status-400');
const http500 = require('./integration/http-status-500');
const errorFlight = require('./integration/error-in-flight');
const customTripper = require('./integration/custom-tripper');

const client = options => new Promise((resolve) => {
fetch(`http://${options.host}:${options.port}/`)
Expand Down Expand Up @@ -123,6 +124,21 @@ test('integration - node-fetch - error', async (t) => {
t.end();
});

test('integration - node-fetch - custom tripper', async (t) => {
const result = await customTripper(client);
t.deepEqual(result, [
'ok',
'ok',
'http error',
'http error',
'http error',
'http error',
'ok',
'ok',
]);
t.end();
});

test('after', async (t) => {
await after();
t.end();
Expand Down
16 changes: 16 additions & 0 deletions test/integration-got.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const http500 = require('./integration/http-status-500');
const http500Retry = require('./integration/http-status-500-retry');
const errorFlight = require('./integration/error-in-flight');
const errorFlightRetry = require('./integration/error-in-flight-retry');
const customTripper = require('./integration/custom-tripper');


const client = async (options) => {
Expand Down Expand Up @@ -129,6 +130,21 @@ test('integration - got - retry: 0 - error', async (t) => {
t.end();
});

test('integration - got - retry: 0 - custom tripper', async (t) => {
const result = await customTripper(client);
t.deepEqual(result, [
'ok',
'ok',
'http error',
'http error',
'http error',
'http error',
'ok',
'ok',
]);
t.end();
});

test('integration - got - retry: default(2) - http status 500 errors', async (t) => {
const result = await http500Retry(client);
t.deepEqual(result, [
Expand Down
15 changes: 15 additions & 0 deletions test/integration-http.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const timeout = require('./integration/timeout');
const http400 = require('./integration/http-status-400');
const http500 = require('./integration/http-status-500');
const errorFlight = require('./integration/error-in-flight');
const customTripper = require('./integration/custom-tripper');

test('before', async (t) => {
await before();
Expand Down Expand Up @@ -107,6 +108,20 @@ test('integration - http.get - error', async (t) => {
t.end();
});

test('integration - http.get - custom tripper', async (t) => {
const result = await customTripper(clientHttp);
t.deepEqual(result, [
'ok',
'ok',
'http error',
'http error',
'http error',
'http error',
'ok',
'ok',
]);
t.end();
});

test('after', async (t) => {
await after();
Expand Down
Loading

0 comments on commit 54f4d8a

Please sign in to comment.