Skip to content

Commit

Permalink
feat: add option to auto-recreate AbortController if aborted during s…
Browse files Browse the repository at this point in the history
…pecific states

When state changes to 'halfOpen' or 'close', the AbortController will be recreated to handle reuse.

nodeshift#861
  • Loading branch information
WillianAgostini committed Oct 23, 2024
1 parent 0a0d75e commit a0117d7
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 0 deletions.
43 changes: 43 additions & 0 deletions lib/circuit.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ Please use options.errorThresholdPercentage`;
* This option allows you to provide an EventEmitter that rotates the buckets
* so you can have one global timer in your app. Make sure that you are
* emitting a 'rotate' event from this EventEmitter
* @param {boolean} options.autoRenewAbortController Automatically recreates the instance
* of AbortController whenever the circuit transitions to 'halfOpen' or 'closed'
* state. This ensures that new requests are not impacted by previous signals
* that were triggered when the circuit was 'open'. Default: false
*
*
* @fires CircuitBreaker#halfOpen
Expand Down Expand Up @@ -193,6 +197,10 @@ class CircuitBreaker extends EventEmitter {
);
}

if (options.autoRenewAbortController && !options.abortController) {
options.abortController = new AbortController();
}

if (options.abortController && typeof options.abortController.abort !== 'function') {
throw new TypeError(
'AbortController does not contain `abort()` method'
Expand Down Expand Up @@ -295,6 +303,9 @@ class CircuitBreaker extends EventEmitter {
function _halfOpen (circuit) {
circuit[STATE] = HALF_OPEN;
circuit[PENDING_CLOSE] = true;
if (circuit.options.autoRenewAbortController) {
circuit.options.abortController = new AbortController();
}
/**
* Emitted after `options.resetTimeout` has elapsed, allowing for
* a single attempt to call the service again. If that attempt is
Expand Down Expand Up @@ -347,6 +358,13 @@ class CircuitBreaker extends EventEmitter {
}
this[STATE] = CLOSED;
this[PENDING_CLOSE] = false;
if (
this.options.autoRenewAbortController &&
this.options.abortController &&
this.options.abortController.signal.aborted
) {
this.options.abortController = new AbortController();
}
/**
* Emitted when the breaker is reset allowing the action to execute again
* @event CircuitBreaker#close
Expand Down Expand Up @@ -810,6 +828,31 @@ class CircuitBreaker extends EventEmitter {
this[ENABLED] = false;
this.status.removeRotateBucketControllerListener();
}

/**
* Retrieves the current AbortSignal from the abortController, if available.
* This signal can be used to monitor ongoing requests.
* @returns {AbortSignal|undefined} The AbortSignal if present,
* otherwise undefined.
*/
getSignal () {
if (this.options.abortController && this.options.abortController.signal) {
return this.options.abortController.signal;
}

return undefined;
}

/**
* Retrieves the current AbortController instance.
* This controller can be used to manually abort ongoing requests or create
* a new signal.
* @returns {AbortController|undefined} The AbortController if present,
* otherwise undefined.
*/
getAbortController () {
return this.options.abortController;
}
}

function handleError (error, circuit, timeout, args, latency, resolve, reject) {
Expand Down
71 changes: 71 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,77 @@ test('When options.abortController is provided, abort controller should not be a
.then(t.end);
});

test('When options.autoRenewAbortController is not provided, signal should not be provided', t => {
t.plan(1);

const breaker = new CircuitBreaker(
passFail
);

const signal = breaker.getSignal();
t.false(signal, 'AbortSignal is empty');

breaker.shutdown();
t.end();
});

test('When options.autoRenewAbortController is provided, signal should be provided', t => {
t.plan(1);

const breaker = new CircuitBreaker(
passFail,
{ autoRenewAbortController: true }
);

const signal = breaker.getSignal();
t.true(signal, 'AbortSignal has instance');

breaker.shutdown();
t.end();
});

test('When autoRenewAbortController option is provided, the signal should be reset', t => {
t.plan(1);

const breaker = new CircuitBreaker(
passFail,
{
autoRenewAbortController: true,
resetTimeout: 10,
timeout: 1
}
);

breaker.fire(10)
.catch(() => new Promise(resolve => {
const signal = breaker.getSignal();
t.true(signal.aborted, 'AbortSignal is aborted after timeout');
setTimeout(() => {
const signal = breaker.getSignal();
t.false(signal.aborted, 'A new AbortSignal is created upon half-open state');
resolve();
}, 20);
})).finally(() => {
breaker.shutdown();
t.end();
});
});

test('When options.autoRenewAbortController is provided, abortController should be provided', t => {
t.plan(1);

const breaker = new CircuitBreaker(
passFail,
{ autoRenewAbortController: true }
);

const ab = breaker.getAbortController();
t.true(ab, 'AbortController has instance');

breaker.shutdown();
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 a0117d7

Please sign in to comment.