Skip to content

Commit

Permalink
feat: adds AbortController support (#674)
Browse files Browse the repository at this point in the history
This commit adds `AbortController` support in Opossum. This is useful in
conjunction with the timeout feature, where Opossum will signal upon
timeout and allow users to cleanly abort their ongoing requests.

See links:
- https://developer.mozilla.org/en-US/docs/Web/API/AbortController
- https://nodejs.org/api/globals.html#class-abortcontroller
- https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#aborting_a_fetch
- https://nodejs.org/api/http.html#httprequesturl-options-callback
  • Loading branch information
raytung committed Sep 27, 2022
1 parent eb6e395 commit 4b5f9f6
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 0 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,44 @@ breaker.fire(x, y)
.catch(console.error);
```

### AbortController support

You can provide an `AbortController` (https://developer.mozilla.org/en-US/docs/Web/API/AbortController, https://nodejs.org/docs/latest/api/globals.html#globals_class_abortcontroller) for aborting on going request upon
reaching Opossum timeout.

```javascript
const CircuitBreaker = require('opossum');
const http = require('http);
function asyncFunctionThatCouldFail(abortSignal, x, y) {
return new Promise((resolve, reject) => {
http.get(
'http://httpbin.org/delay/10',
{ signal: abortSignal },
(res) => {
if(res.statusCode < 300) {
resolve(res.statusCode);
return;
}

reject(res.statusCode);
}
);
});
}

const abortController = new AbortController();
const options = {
abortController,
timeout: 3000, // If our function takes longer than 3 seconds, trigger a failure
};
const breaker = new CircuitBreaker(asyncFunctionThatCouldFail, options);

breaker.fire(abortController.signal)
.then(console.log)
.catch(console.error);
```

### Fallback

You can also provide a fallback function that will be executed in the
Expand Down
12 changes: 12 additions & 0 deletions lib/circuit.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ Please use options.errorThresholdPercentage`;
* has been cached that value will be returned for every subsequent execution:
* the cache can be cleared using `clearCache`. (The metrics `cacheHit` and
* `cacheMiss` reflect cache activity.) Default: false
* @param {AbortController} options.abortController this allows Opossum to
* signal upon timeout and properly abort your on going requests instead of
* leaving it in the background
*
*
* @fires CircuitBreaker#halfOpen
Expand Down Expand Up @@ -152,6 +155,12 @@ class CircuitBreaker extends EventEmitter {
);
}

if (options.abortController && typeof options.abortController.abort !== 'function') {
throw new TypeError(
'AbortController does not contain `abort()` method'
);
}

this[VOLUME_THRESHOLD] = Number.isInteger(options.volumeThreshold)
? options.volumeThreshold
: 0;
Expand Down Expand Up @@ -598,6 +607,9 @@ class CircuitBreaker extends EventEmitter {
*/
this.emit('timeout', error, latency, args);
handleError(error, this, timeout, args, latency, resolve, reject);
if (this.options.abortController) {
this.options.abortController.abort();
}
}, this.options.timeout);
}

Expand Down
68 changes: 68 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,74 @@ test('Fails when the circuit function times out', t => {
.then(t.end);
});

test('When options.abortController is provided but does not contain an `abort()` function', t => {
t.plan(2);
const abortController = {};

try {
const _ = new CircuitBreaker(passFail, { abortController });

t.fail('Did not throw TypeError on instantiation');
} catch (e) {
t.equals(e.constructor, TypeError, 'throws TypeError');
t.equals(e.message, 'AbortController does not contain `abort()` method');
}

t.end();
});

test('When options.abortController is provided, the circuit breaker should call abort() upon timeout', t => {
t.plan(1);

let spyAbortCalled = false;

// Did not provide the AbortController instance here as NodeJS 14 doesn't
// support AbortController but for all intents and purposes, a class or object
// with the `abort()` method should be supported.
const abortController = {
abort: () => {
spyAbortCalled = true;
}
};

const breaker = new CircuitBreaker(
slowFunction,
{ timeout: 10, abortController }
);

breaker.fire()
.then(t.fail)
.catch(e => {
t.true(spyAbortCalled, 'AbortController.abort() was not called upon timeout');
})
.then(_ => breaker.shutdown())
.then(t.end);
});

test('When options.abortController is provided, abort controller should not be aborted if request completes before timeout', t => {
t.plan(1);

let spyAbortCalled = false;
const abortController = {
abort: () => {
spyAbortCalled = true;
}
};

const breaker = new CircuitBreaker(
passFail,
{ abortController }
);

breaker.fire(10)
.catch(t.fail)
.then(() => {
t.false(spyAbortCalled, 'AbortController.abort() was called when it shouldn\'t have');
})
.then(_ => breaker.shutdown())
.then(t.end);
});

test('Works with functions that do not return a promise', t => {
t.plan(1);
const breaker = new CircuitBreaker(nonPromise);
Expand Down

0 comments on commit 4b5f9f6

Please sign in to comment.