2024-01-01 21:11:32 +00:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
2023-03-18 12:36:00 +00:00
|
|
|
// This module is browser compatible.
|
2022-11-30 18:45:25 +00:00
|
|
|
|
2023-12-04 04:42:01 +00:00
|
|
|
import { exponentialBackoffWithJitter } from "./_util.ts";
|
2023-05-20 12:14:18 +00:00
|
|
|
|
2023-12-01 02:19:22 +00:00
|
|
|
/**
|
|
|
|
* Error thrown in {@linkcode retry} once the maximum number of failed attempts
|
|
|
|
* has been reached.
|
2024-05-22 00:40:43 +00:00
|
|
|
*
|
2024-05-22 05:08:36 +00:00
|
|
|
* @example Usage
|
2024-06-03 04:10:27 +00:00
|
|
|
* ```ts no-assert no-eval
|
2024-05-22 00:40:43 +00:00
|
|
|
* import { RetryError } from "@std/async/retry";
|
|
|
|
*
|
2024-06-03 04:10:27 +00:00
|
|
|
* throw new RetryError({ foo: "bar" }, 3);
|
2024-05-22 00:40:43 +00:00
|
|
|
* ```
|
2023-12-01 02:19:22 +00:00
|
|
|
*/
|
2022-11-30 18:45:25 +00:00
|
|
|
export class RetryError extends Error {
|
2023-12-01 02:19:22 +00:00
|
|
|
/**
|
|
|
|
* Constructs a new {@linkcode RetryError} instance.
|
|
|
|
*
|
2024-05-22 00:40:43 +00:00
|
|
|
* @param cause the cause for this error.
|
|
|
|
* @param attempts the number of retry attempts made.
|
2023-12-01 02:19:22 +00:00
|
|
|
*/
|
2023-06-10 10:01:46 +00:00
|
|
|
constructor(cause: unknown, attempts: number) {
|
|
|
|
super(`Retrying exceeded the maxAttempts (${attempts}).`);
|
2022-11-30 18:45:25 +00:00
|
|
|
this.name = "RetryError";
|
|
|
|
this.cause = cause;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-01 02:19:22 +00:00
|
|
|
/** Options for {@linkcode retry}. */
|
2022-11-30 18:45:25 +00:00
|
|
|
export interface RetryOptions {
|
2023-12-01 02:19:22 +00:00
|
|
|
/**
|
|
|
|
* How much to backoff after each retry.
|
|
|
|
*
|
|
|
|
* @default {2}
|
|
|
|
*/
|
2022-11-30 18:45:25 +00:00
|
|
|
multiplier?: number;
|
2023-12-01 02:19:22 +00:00
|
|
|
/**
|
|
|
|
* The maximum milliseconds between attempts.
|
|
|
|
*
|
|
|
|
* @default {60000}
|
|
|
|
*/
|
2022-11-30 18:45:25 +00:00
|
|
|
maxTimeout?: number;
|
2023-12-01 02:19:22 +00:00
|
|
|
/**
|
|
|
|
* The maximum amount of attempts until failure.
|
|
|
|
*
|
|
|
|
* @default {5}
|
|
|
|
*/
|
2022-11-30 18:45:25 +00:00
|
|
|
maxAttempts?: number;
|
2023-12-01 02:19:22 +00:00
|
|
|
/**
|
|
|
|
* The initial and minimum amount of milliseconds between attempts.
|
|
|
|
*
|
|
|
|
* @default {1000}
|
|
|
|
*/
|
2022-11-30 18:45:25 +00:00
|
|
|
minTimeout?: number;
|
2023-12-01 02:19:22 +00:00
|
|
|
/**
|
|
|
|
* Amount of jitter to introduce to the time between attempts. This is `1`
|
|
|
|
* for full jitter by default.
|
|
|
|
*
|
|
|
|
* @default {1}
|
|
|
|
*/
|
2023-06-07 10:46:00 +00:00
|
|
|
jitter?: number;
|
2022-11-30 18:45:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-06-07 10:46:00 +00:00
|
|
|
* Calls the given (possibly asynchronous) function up to `maxAttempts` times.
|
2023-12-01 02:19:22 +00:00
|
|
|
* Retries as long as the given function throws. If the attempts are exhausted,
|
|
|
|
* throws a {@linkcode RetryError} with `cause` set to the inner exception.
|
2023-06-07 10:46:00 +00:00
|
|
|
*
|
2024-01-08 13:34:09 +00:00
|
|
|
* The backoff is calculated by multiplying `minTimeout` with `multiplier` to the power of the current attempt counter (starting at 0 up to `maxAttempts - 1`). It is capped at `maxTimeout` however.
|
|
|
|
* How long the actual delay is, depends on `jitter`.
|
2023-06-07 10:46:00 +00:00
|
|
|
*
|
2023-12-01 02:19:22 +00:00
|
|
|
* When `jitter` is the default value of `1`, waits between two attempts for a
|
|
|
|
* randomized amount between 0 and the backoff time. With the default options
|
|
|
|
* the maximal delay will be `15s = 1s + 2s + 4s + 8s`. If all five attempts
|
|
|
|
* are exhausted the mean delay will be `9.5s = ½(4s + 15s)`.
|
2023-06-07 10:46:00 +00:00
|
|
|
*
|
|
|
|
* When `jitter` is `0`, waits the full backoff time.
|
2022-11-30 18:45:25 +00:00
|
|
|
*
|
2024-05-22 05:08:36 +00:00
|
|
|
* @example Example configuration 1
|
2024-06-03 04:10:27 +00:00
|
|
|
* ```ts no-assert
|
2024-04-29 02:57:30 +00:00
|
|
|
* import { retry } from "@std/async/retry";
|
2022-11-30 18:45:25 +00:00
|
|
|
* const req = async () => {
|
|
|
|
* // some function that throws sometimes
|
|
|
|
* };
|
|
|
|
*
|
|
|
|
* // Below resolves to the first non-error result of `req`
|
|
|
|
* const retryPromise = await retry(req, {
|
|
|
|
* multiplier: 2,
|
|
|
|
* maxTimeout: 60000,
|
|
|
|
* maxAttempts: 5,
|
|
|
|
* minTimeout: 100,
|
2023-06-07 10:46:00 +00:00
|
|
|
* jitter: 1,
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
2024-05-22 05:08:36 +00:00
|
|
|
* @example Example configuration 2
|
2024-06-03 04:10:27 +00:00
|
|
|
* ```ts no-assert
|
2024-04-29 02:57:30 +00:00
|
|
|
* import { retry } from "@std/async/retry";
|
2023-06-07 10:46:00 +00:00
|
|
|
* const req = async () => {
|
|
|
|
* // some function that throws sometimes
|
|
|
|
* };
|
|
|
|
*
|
|
|
|
* // Make sure we wait at least 1 minute, but at most 2 minutes
|
|
|
|
* const retryPromise = await retry(req, {
|
|
|
|
* multiplier: 2.34,
|
|
|
|
* maxTimeout: 80000,
|
|
|
|
* maxAttempts: 7,
|
|
|
|
* minTimeout: 1000,
|
|
|
|
* jitter: 0.5,
|
2022-11-30 18:45:25 +00:00
|
|
|
* });
|
2023-05-20 12:14:18 +00:00
|
|
|
* ```
|
2024-05-22 00:40:43 +00:00
|
|
|
*
|
|
|
|
* @typeParam T The return type of the function to retry and returned promise.
|
|
|
|
* @param fn The function to retry.
|
2024-09-03 08:57:27 +00:00
|
|
|
* @param options Additional options.
|
2024-05-22 00:40:43 +00:00
|
|
|
* @returns The promise that resolves with the value returned by the function to retry.
|
2022-11-30 18:45:25 +00:00
|
|
|
*/
|
|
|
|
export async function retry<T>(
|
|
|
|
fn: (() => Promise<T>) | (() => T),
|
2024-09-03 08:57:27 +00:00
|
|
|
options?: RetryOptions,
|
2023-12-01 02:19:22 +00:00
|
|
|
): Promise<T> {
|
2024-09-03 08:57:27 +00:00
|
|
|
const {
|
|
|
|
multiplier = 2,
|
|
|
|
maxTimeout = 60000,
|
|
|
|
maxAttempts = 5,
|
|
|
|
minTimeout = 1000,
|
|
|
|
jitter = 1,
|
|
|
|
} = options ?? {};
|
2022-11-30 18:45:25 +00:00
|
|
|
|
2024-09-03 08:57:27 +00:00
|
|
|
if (maxTimeout <= 0) {
|
2024-08-22 06:02:58 +00:00
|
|
|
throw new TypeError(
|
2024-09-03 08:57:27 +00:00
|
|
|
`Cannot retry as 'maxTimeout' must be positive: current value is ${maxTimeout}`,
|
2024-08-22 06:02:58 +00:00
|
|
|
);
|
|
|
|
}
|
2024-09-03 08:57:27 +00:00
|
|
|
if (minTimeout > maxTimeout) {
|
2024-08-22 06:02:58 +00:00
|
|
|
throw new TypeError(
|
2024-09-03 08:57:27 +00:00
|
|
|
`Cannot retry as 'minTimeout' must be <= 'maxTimeout': current values 'minTimeout=${minTimeout}', 'maxTimeout=${maxTimeout}'`,
|
2024-08-22 06:02:58 +00:00
|
|
|
);
|
|
|
|
}
|
2024-09-03 08:57:27 +00:00
|
|
|
if (jitter > 1) {
|
2024-08-22 06:02:58 +00:00
|
|
|
throw new TypeError(
|
2024-09-03 08:57:27 +00:00
|
|
|
`Cannot retry as 'jitter' must be <= 1: current value is ${jitter}`,
|
2024-08-22 06:02:58 +00:00
|
|
|
);
|
2024-06-05 01:13:11 +00:00
|
|
|
}
|
2022-11-30 18:45:25 +00:00
|
|
|
|
2023-06-07 10:46:00 +00:00
|
|
|
let attempt = 0;
|
|
|
|
while (true) {
|
2022-11-30 18:45:25 +00:00
|
|
|
try {
|
|
|
|
return await fn();
|
2023-06-07 10:46:00 +00:00
|
|
|
} catch (error) {
|
2024-09-03 08:57:27 +00:00
|
|
|
if (attempt + 1 >= maxAttempts) {
|
|
|
|
throw new RetryError(error, maxAttempts);
|
2023-06-07 10:46:00 +00:00
|
|
|
}
|
2023-05-20 12:14:18 +00:00
|
|
|
|
2023-12-04 04:42:01 +00:00
|
|
|
const timeout = exponentialBackoffWithJitter(
|
2024-09-03 08:57:27 +00:00
|
|
|
maxTimeout,
|
|
|
|
minTimeout,
|
2023-06-07 10:46:00 +00:00
|
|
|
attempt,
|
2024-09-03 08:57:27 +00:00
|
|
|
multiplier,
|
|
|
|
jitter,
|
2023-05-20 12:14:18 +00:00
|
|
|
);
|
2023-06-07 10:46:00 +00:00
|
|
|
await new Promise((r) => setTimeout(r, timeout));
|
2022-11-30 18:45:25 +00:00
|
|
|
}
|
2023-06-07 10:46:00 +00:00
|
|
|
attempt++;
|
2022-11-30 18:45:25 +00:00
|
|
|
}
|
|
|
|
}
|