Add abort signal to exponential backoff
This commit is contained in:
@@ -7,3 +7,16 @@ export class ExponentialBackoffMaxRetriesHitError extends Error {
|
|||||||
this.name = 'ExponentialBackoffMaxRetriesHitError';
|
this.name = 'ExponentialBackoffMaxRetriesHitError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when the exponential backoff retries are stopped
|
||||||
|
*/
|
||||||
|
export class ExponentialBackoffStoppedRetriesError extends Error {
|
||||||
|
constructor(reason: unknown) {
|
||||||
|
// Convert the reason to an error if it is not an error
|
||||||
|
const reasonError = reason instanceof Error ? reason : new Error(`${reason}`);
|
||||||
|
|
||||||
|
super(`Exponential backoff was aborted: "${reasonError.message}"`, { cause: reasonError });
|
||||||
|
this.name = 'ExponentialBackoffStoppedRetriesError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExponentialBackoffMaxRetriesHitError } from './errors.ts';
|
import { ExponentialBackoffStoppedRetriesError, ExponentialBackoffMaxRetriesHitError } from './errors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exponential backoff is a technique used to retry a function after a delay.
|
* Exponential backoff is a technique used to retry a function after a delay.
|
||||||
@@ -29,11 +29,15 @@ export class ExponentialBackoff {
|
|||||||
* @param onError - The callback to call when an error occurs
|
* @param onError - The callback to call when an error occurs
|
||||||
* @param options - The configuration for the exponential backoff
|
* @param options - The configuration for the exponential backoff
|
||||||
*
|
*
|
||||||
* @throws The last error if the function fails and we have hit the max attempts
|
* @throws An {@link ExponentialBackoffMaxRetriesHitError} with all the errors that were thrown by the task function
|
||||||
*
|
*
|
||||||
* @returns The result of the function
|
* @returns The result of the function
|
||||||
*/
|
*/
|
||||||
static run<T>(taskFn: () => Promise<T>, onError = (_error: Error): void => {}, options?: Partial<ExponentialBackoffOptions>): Promise<T> {
|
static run<T>(
|
||||||
|
taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise<T>,
|
||||||
|
onError = (_error: Error): void => {},
|
||||||
|
options?: Partial<ExponentialBackoffOptions>,
|
||||||
|
): Promise<T> {
|
||||||
const backoff = ExponentialBackoff.from(options);
|
const backoff = ExponentialBackoff.from(options);
|
||||||
|
|
||||||
return backoff.run(taskFn, onError);
|
return backoff.run(taskFn, onError);
|
||||||
@@ -75,34 +79,62 @@ export class ExponentialBackoff {
|
|||||||
* @param fn - The function to run
|
* @param fn - The function to run
|
||||||
* @param onError - The callback to call when an error occurs
|
* @param onError - The callback to call when an error occurs
|
||||||
*
|
*
|
||||||
* @throws An ExponentialBackoffMaxRetriesHitError with all the errors that were thrown by the task function
|
* @throws An {@link ExponentialBackoffMaxRetriesHitError} with all the errors that were thrown by the task function
|
||||||
*
|
*
|
||||||
* @returns The result of the function
|
* @returns The result of the function
|
||||||
*/
|
*/
|
||||||
async run<T>(taskFn: () => Promise<T>, onError = (_error: Error): void => {}): Promise<T> {
|
async run<T>(
|
||||||
|
taskFn: (callbackParameters: ExponentialBackoffCallbackParameters) => Promise<T>,
|
||||||
|
onError = (_error: Error): void => {},
|
||||||
|
): Promise<T> {
|
||||||
|
// Initialize an abort signal to allow the task function to be aborted
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const stopRetries = abortController.abort.bind(abortController);
|
||||||
|
|
||||||
// Initialize an empty array to store the errors
|
// Initialize an empty array to store the errors
|
||||||
const errors: Error[] = [];
|
const errors: Error[] = [];
|
||||||
|
|
||||||
|
// Initialize the attempt counter
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
|
||||||
// If the max attempts is 0, we should continue indefinitely.
|
// If the max attempts is 0, we should continue indefinitely.
|
||||||
const unlimitedAttempts = this.options.maxAttempts === 0;
|
const unlimitedAttempts = this.options.maxAttempts === 0;
|
||||||
|
|
||||||
while (unlimitedAttempts || attempt < this.options.maxAttempts) {
|
// Loop until we succeed, hit the max attempts, or the abort signal is activated
|
||||||
|
while (true) {
|
||||||
try {
|
try {
|
||||||
// Await the promise before returning so its execution context remains in the try-catch
|
// Await the promise before returning so its execution context remains in the try-catch
|
||||||
// If we didn't await, this `run` function would successfully return and any errors would not be caught here.
|
// If we didn't await, this `run` function would successfully return and any errors would not be caught here.
|
||||||
return await taskFn();
|
return await taskFn({ stopRetries });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Store the error in case we fail every attempt
|
// Store the error in case we fail every attempt
|
||||||
const errorInstance = error instanceof Error ? error : new Error(`${error}`);
|
const errorInstance = error instanceof Error ? error : new Error(`${error}`);
|
||||||
errors.push(errorInstance);
|
|
||||||
onError(errorInstance);
|
onError(errorInstance);
|
||||||
|
|
||||||
|
// If we have unlimited attemps, don't append this to the errors array to prevent a memory leak.
|
||||||
|
if (!unlimitedAttempts) {
|
||||||
|
errors.push(errorInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the count for next attempt. Do this now so we can exit before waiting and before running the next attempt.
|
||||||
|
const nextAttemptCount = attempt + 1;
|
||||||
|
const nextAttemptExceedsMaxAttempts = nextAttemptCount >= this.options.maxAttempts;
|
||||||
|
|
||||||
|
// If the next attempt exceeds the max attempts, break out of the loop
|
||||||
|
if (!unlimitedAttempts && nextAttemptExceedsMaxAttempts) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the abort signal has been aborted
|
||||||
|
if (abortController.signal.aborted) {
|
||||||
|
// Throw an error if the abort signal has been aborted
|
||||||
|
throw new ExponentialBackoffStoppedRetriesError(abortController.signal.reason);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait before going to the next attempt
|
// Wait before going to the next attempt
|
||||||
const delay = ExponentialBackoff.calculateDelay(this.options, attempt);
|
const delay = ExponentialBackoff.calculateDelay(this.options, attempt);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
}
|
|
||||||
|
|
||||||
attempt++;
|
attempt++;
|
||||||
}
|
}
|
||||||
@@ -114,7 +146,8 @@ export class ExponentialBackoff {
|
|||||||
/**
|
/**
|
||||||
* Calculate the delay before we should attempt to retry
|
* Calculate the delay before we should attempt to retry
|
||||||
*
|
*
|
||||||
* @param attempt
|
* @param options - The configuration for the exponential backoff
|
||||||
|
* @param attempt - The current attempt number
|
||||||
* @returns The time in milliseconds before another attempt should be made
|
* @returns The time in milliseconds before another attempt should be made
|
||||||
*/
|
*/
|
||||||
public static calculateDelay(options: ExponentialBackoffOptions, attempt: number): number {
|
public static calculateDelay(options: ExponentialBackoffOptions, attempt: number): number {
|
||||||
@@ -165,3 +198,20 @@ export type ExponentialBackoffOptions = {
|
|||||||
*/
|
*/
|
||||||
jitter: number;
|
jitter: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function to call to stop the retries.
|
||||||
|
* This mimics the AbortSignal.abort function by taking in a reason for stopping
|
||||||
|
*
|
||||||
|
* @param reason - The reason for stopping the retries.
|
||||||
|
*/
|
||||||
|
export type ExponentialBackoffStopRetriesFunction = (reason: unknown) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameters for the task function
|
||||||
|
*
|
||||||
|
* @param stopRetries - The function to call to stop the retries
|
||||||
|
*/
|
||||||
|
export type ExponentialBackoffCallbackParameters = {
|
||||||
|
stopRetries: ExponentialBackoffStopRetriesFunction;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user