Skip to content

Commit

Permalink
Merge pull request #52 from bbc/IPLAYER-45725-circuit-breaker-mware
Browse files Browse the repository at this point in the history
Iplayer 45725 circuit breaker mware
  • Loading branch information
chr15stevens authored Nov 27, 2024
2 parents 34baa2a + e675152 commit ce1a374
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 4 deletions.
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export declare class HttpTransportBuilder<
userAgent(userAgent: string): HttpTransportBuilder<ContextCurrent>;
retries(retries: number): HttpTransportBuilder<ContextCurrent>;
retryDelay(retryDelay: number): HttpTransportBuilder<ContextCurrent>;
criticalErrorDetector(criticalErrorDetector: (err, ctx: Context) => boolean);
use<ContextExtra = {}>(
fn: Plugin<ContextExtra, ContextCurrent>
): HttpTransportBuilder<ContextExtra & ContextCurrent>;
Expand Down Expand Up @@ -141,7 +142,7 @@ export declare class HttpTransportClient<
ResponseBody
>;
asResponse<ResponseBody = ContextCurrent["res"]["body"]>(): Promise<
AsResponse<ContextCurrent["res"], ResponseBody>
AsResponse<ContextCurrent["res"], ResponseBody>
>;
}

Expand Down
30 changes: 30 additions & 0 deletions lib/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,36 @@ class HttpTransportBuilder {
return this;
}

/**
* default the criticalErrorDetector which parses errors and decides if they are critical. This mainly enables retry logic.
* @param {function} criticalErrorDetector (err, ctx) => bool - a function that takes the error and the context and returns a boolean which evaluates whether an error is a critical error.
* This is useful if you want to customise error behaviour. See below for example that prevents circuitBreaker errors from being classed as critical errors.
* criticalErrors trigger retry behaviour and so may not be desirable in all scenarios.
* @return a HttpTransportBuilder instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const builder = httpTransport.createBuilder();
* builder.criticalErrorDetector(() => {
* if (err && (err.statusCode < 500 || err.isBrokenCircuitError)) {
* return false;
* }
* return true;
* });
*
* @default
* (err, ctx) => {
* if (err && err.statusCode < 500) {
* return false;
* }
* return true;
* }
*/
criticalErrorDetector(criticalErrorDetector) {
_.set(this._defaults, 'ctx.criticalErrorDetector', criticalErrorDetector);
return this;
}

/**
* Registers a global plugin, which is used for all requests
*
Expand Down
8 changes: 5 additions & 3 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,9 @@ class HttpTransportClient {
}
}

function isCriticalError(err) {
function isCriticalError(err, ctx) {
if (ctx.criticalErrorDetector) return ctx.criticalErrorDetector(err, ctx);

if (err && err.statusCode < 500) {
return false;
}
Expand All @@ -363,14 +365,14 @@ function retry(fn, ctx) {
function attempt(i) {
return fn(ctx)
.catch((err) => {
if (i < maxAttempts && isCriticalError(err) && ctx.retryDelay && ctx.retryDelay > 0) {
if (i < maxAttempts && isCriticalError(err, ctx) && ctx.retryDelay && ctx.retryDelay > 0) {
const delayBy = rejectedPromise(ctx.retryDelay);
return delayBy(err);
}
throw err;
})
.catch((err) => {
if (i < maxAttempts && isCriticalError(err)) {
if (i < maxAttempts && isCriticalError(err, ctx)) {
ctx.retryAttempts.push(toRetry(err));
return attempt(++i);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Context {
this.plugins = [];
this.req = Request.create();
this.res = Response.create();
this.criticalErrorDetector = undefined;
if (defaults) this._applyDefaults(defaults);
}

Expand All @@ -42,6 +43,7 @@ class Context {
this.userAgent = defaults.ctx?.userAgent || USER_AGENT;
this.retries = defaults.ctx?.retries || RETRIES;
this.retryDelay = defaults.ctx?.retryDelay || RETRY_DELAY;
this.criticalErrorDetector = defaults.ctx?.criticalErrorDetector;
}

static create(defaults) {
Expand Down
16 changes: 16 additions & 0 deletions test/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,22 @@ describe('HttpTransportClient', () => {
assert.equal(ctx.retries, 50);
assert.equal(ctx.retryDelay, 2000);
});

it('sets default criticalErrorDetector in the context', async () => {
const transport = new Transport();
sandbox.stub(transport, 'execute').returns(Promise.resolve());

const client = HttpTransport.createBuilder(transport)
.criticalErrorDetector(() => false)
.createClient();

await client
.get(url)
.asResponse();

const ctx = transport.execute.getCall(0).args[0];
assert.equal(ctx.criticalErrorDetector.toString(), (() => false).toString());
});
});

describe('.retries', () => {
Expand Down

0 comments on commit ce1a374

Please sign in to comment.