From 08fa2c96c699875ddd05c85f36d2a3a7cfc73e50 Mon Sep 17 00:00:00 2001 From: kim-sung-jee Date: Mon, 20 Jan 2025 13:44:55 +0900 Subject: [PATCH 1/4] feat(retry-backoff-jitter): add options for backoff and jitter --- src/function/retry.spec.ts | 6 ++-- src/function/retry.ts | 65 +++++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/function/retry.spec.ts b/src/function/retry.spec.ts index c3a7d7ca7..7df5628b2 100644 --- a/src/function/retry.spec.ts +++ b/src/function/retry.spec.ts @@ -22,13 +22,13 @@ describe('retry', () => { it('should retry with the specified delay between attempts', async () => { const func = vi.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValue('success'); - const delay = 100; + const retryMinDelay = 100; const start = Date.now(); - const result = await retry(func, { delay, retries: 2 }); + const result = await retry(func, { retryMinDelay, retries: 2 }); const end = Date.now(); expect(result).toBe('success'); expect(func).toHaveBeenCalledTimes(2); - expect(end - start).toBeGreaterThanOrEqual(delay); + expect(end - start).toBeGreaterThanOrEqual(retryMinDelay); }); it('should throw an error after the specified number of retries', async () => { diff --git a/src/function/retry.ts b/src/function/retry.ts index e3a848866..d0b77a6af 100644 --- a/src/function/retry.ts +++ b/src/function/retry.ts @@ -1,11 +1,30 @@ +import { isNumber as isNumberToolkit } from '../compat'; import { delay as delayToolkit } from '../promise'; interface RetryOptions { /** - * The number of milliseconds to interval delay. + * The minimum delay in milliseconds before starting retries. * @default 0 */ - delay?: number; + retryMinDelay?: number; + + /** + * The maximum delay in milliseconds between retries. + * @default Infinity + */ + retryMaxDelay?: number; + + /** + * The exponential factor to use for increasing retry delay. + * @default 2 + */ + factor?: number; + + /** + * Whether to randomize the retry delay by a factor between 1 and 2. + * @default false + */ + randomize?: boolean; /** * The number of retries to attempt. @@ -19,8 +38,11 @@ interface RetryOptions { signal?: AbortSignal; } -const DEFAULT_DELAY = 0; const DEFAULT_RETRIES = Number.POSITIVE_INFINITY; +const DEFAULT_MIN_DELAY = 0; +const DEFAULT_MAX_DELAY = Number.POSITIVE_INFINITY; +const DEFAULT_FACTOR = 2; +const DEFAULT_RANDOMIZE = false; /** * Retries a function that returns a promise until it resolves successfully. @@ -55,14 +77,19 @@ export async function retry(func: () => Promise, retries: number): Promise * @template T * @param {() => Promise} func - The function to retry. It should return a promise. * @param {RetryOptions} options - Options to configure the retry behavior. - * @param {number} [options.delay=0] - The number of milliseconds to wait between retries. - * @param {number} [options.retries=Infinity] - The number of retries to attempt. + * @param {number} [options.retries=Infinity] - The number of retries to attempt. Default is Infinity. + * @param {number} [options.retryMinDelay=0] - The minimum delay in milliseconds between retry attempts. Default is 0. + * @param {number} [options.retryMaxDelay=Infinity] - The maximum delay in milliseconds between retry attempts. Default is Infinity. + * @param {number} [options.factor=2] - The exponential factor to use for increasing retry delay. Default is 2. + * @param {boolean} [options.randomize=false] - Whether to randomize the retry delay by a factor between 1 and 2. Default is false. * @param {AbortSignal} [options.signal] - An AbortSignal to cancel the retry operation. * @returns {Promise} A promise that resolves with the value of the successful function call. * * @example - * // Retry a function with a delay of 1000ms between attempts - * retry(() => fetchData(), { delay: 1000, times: 5 }).then(data => console.log(data)); + * // Retry a function with fixed delay of 1000ms and no exponential backoff + * retry(() => fetchData(), { retryMinDelay: 1000, retryMaxDelay: 1000, factor: 1, retries: 3 }) + * .then(data => console.log(data)) + * .catch(err => console.error(err)); */ export async function retry(func: () => Promise, options: RetryOptions): Promise; @@ -75,23 +102,32 @@ export async function retry(func: () => Promise, options: RetryOptions): P * @returns {Promise} A promise that resolves with the value of the successful function call. */ export async function retry(func: () => Promise, _options?: number | RetryOptions): Promise { - let delay: number; let retries: number; let signal: AbortSignal | undefined; + let retryMinDelay: number; + let retryMaxDelay: number; + let factor: number; + let randomize: boolean; - if (typeof _options === 'number') { - delay = DEFAULT_DELAY; + if (isNumberToolkit(_options)) { retries = _options; + retryMinDelay = DEFAULT_MIN_DELAY; + retryMaxDelay = DEFAULT_MAX_DELAY; + factor = DEFAULT_FACTOR; + randomize = DEFAULT_RANDOMIZE; signal = undefined; } else { - delay = _options?.delay ?? DEFAULT_DELAY; retries = _options?.retries ?? DEFAULT_RETRIES; + retryMinDelay = _options?.retryMinDelay ?? DEFAULT_MIN_DELAY; + retryMaxDelay = _options?.retryMaxDelay ?? DEFAULT_MAX_DELAY; + factor = _options?.factor ?? DEFAULT_FACTOR; + randomize = _options?.randomize ?? DEFAULT_RANDOMIZE; signal = _options?.signal; } let error; - for (let i = 0; i < retries; i++) { + for (let attempt = 0; attempt < retries; attempt++) { if (signal?.aborted) { throw error ?? new Error(`The retry operation was aborted due to an abort signal.`); } @@ -100,6 +136,11 @@ export async function retry(func: () => Promise, _options?: number | Retry return await func(); } catch (err) { error = err; + + const random = randomize ? Math.random() + 1 : 1; + console.log(err); + const delay = Math.min(Math.round(random * retryMinDelay * Math.pow(factor, attempt)), retryMaxDelay); + await delayToolkit(delay); } } From e4e4b8c376e6edd8dc8c281ec2499f0734cd6120 Mon Sep 17 00:00:00 2001 From: kim-sung-jee Date: Mon, 20 Jan 2025 14:44:30 +0900 Subject: [PATCH 2/4] feat(retry-backoff-jitter): add test for retry --- src/function/retry.spec.ts | 66 +++++++++++++++++++++++++++++++++----- src/function/retry.ts | 2 +- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/function/retry.spec.ts b/src/function/retry.spec.ts index 7df5628b2..293cf0947 100644 --- a/src/function/retry.spec.ts +++ b/src/function/retry.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import { retry } from './retry'; +import * as Delay from '../promise/delay'; describe('retry', () => { it('should resolve successfully on the first attempt', async () => { @@ -15,26 +16,75 @@ describe('retry', () => { .mockRejectedValueOnce(new Error('failure')) .mockRejectedValueOnce(new Error('failure')) .mockResolvedValue('success'); - const result = await retry(func, 3); + const retries = 3; + + const result = await retry(func, retries); expect(result).toBe('success'); - expect(func).toHaveBeenCalledTimes(3); + expect(func).toHaveBeenCalledTimes(retries); }); it('should retry with the specified delay between attempts', async () => { const func = vi.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValue('success'); const retryMinDelay = 100; - const start = Date.now(); - const result = await retry(func, { retryMinDelay, retries: 2 }); - const end = Date.now(); + const retryMaxDelay = 300; + const retries = 2; + const delaySpy = vi.spyOn(Delay, 'delay'); + + const result = await retry(func, { retryMinDelay, retries }); + expect(result).toBe('success'); - expect(func).toHaveBeenCalledTimes(2); - expect(end - start).toBeGreaterThanOrEqual(retryMinDelay); + expect(func).toHaveBeenCalledTimes(retries); + + const calls = delaySpy.mock.calls.map(([ms]) => ms); + calls.forEach(delayTime => { + expect(delayTime).toBeGreaterThanOrEqual(retryMinDelay); + expect(delayTime).toBeLessThanOrEqual(retryMaxDelay); + }); }); it('should throw an error after the specified number of retries', async () => { const func = vi.fn().mockRejectedValue(new Error('failure')); - await expect(retry(func, 3)).rejects.toThrow('failure'); + const retries = 3; + await expect(retry(func, retries)).rejects.toThrow('failure'); + expect(func).toHaveBeenCalledTimes(retries); + }); + + it('should apply randomization to retry delays when randomize is true', async () => { + const func = vi.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValue('success'); + const retryMinDelay = 100; + const retryMaxDelay = 300; + const randomize = true; + const factor = 1; + const retries = 2; + + const delaySpy = vi.spyOn(Delay, 'delay'); + + const result = await retry(func, { retryMinDelay, retryMaxDelay, factor, randomize, retries }); + + expect(func).toHaveBeenCalledTimes(retries); + expect(result).toBe('success'); + + const calls = delaySpy.mock.calls.map(([ms]) => ms); + calls.forEach(delayTime => { + expect(delayTime).toBeGreaterThanOrEqual(retryMinDelay); + expect(delayTime).toBeLessThanOrEqual(retryMaxDelay); + }); + }); + + it('should increase delay exponentially based on the factor', async () => { + const func = vi.fn().mockRejectedValue(new Error('failure')); + const retryMinDelay = 100; + const factor = 2; + const retries = 3; + + const delaySpy = vi.spyOn(Delay, 'delay'); + + await retry(func, { retryMinDelay, factor, retries }).catch(() => {}); + expect(func).toHaveBeenCalledTimes(3); + expect(delaySpy).toHaveBeenCalledWith(100); + expect(delaySpy).toHaveBeenCalledWith(200); + expect(delaySpy).toHaveBeenCalledWith(400); }); it('should abort the retry operation if the signal is already aborted', async () => { diff --git a/src/function/retry.ts b/src/function/retry.ts index d0b77a6af..e3952cc24 100644 --- a/src/function/retry.ts +++ b/src/function/retry.ts @@ -138,7 +138,7 @@ export async function retry(func: () => Promise, _options?: number | Retry error = err; const random = randomize ? Math.random() + 1 : 1; - console.log(err); + const delay = Math.min(Math.round(random * retryMinDelay * Math.pow(factor, attempt)), retryMaxDelay); await delayToolkit(delay); From 0f7ec9ce54732e7a4f1973c2bd6018fb7703ed36 Mon Sep 17 00:00:00 2001 From: kim-sung-jee Date: Mon, 20 Jan 2025 14:50:09 +0900 Subject: [PATCH 3/4] feat(retry-backoff-jitter): default factor value to 1 --- src/function/retry.spec.ts | 34 +++++++++++++++++++++++----------- src/function/retry.ts | 6 +++--- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/function/retry.spec.ts b/src/function/retry.spec.ts index 293cf0947..2b11a1863 100644 --- a/src/function/retry.spec.ts +++ b/src/function/retry.spec.ts @@ -23,6 +23,21 @@ describe('retry', () => { expect(func).toHaveBeenCalledTimes(retries); }); + it('should retry with fixed delay when factor is 1 and randomize is false', async () => { + const func = vi.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValue('success'); + const retryMinDelay = 100; + const retries = 2; + const delaySpy = vi.spyOn(Delay, 'delay'); + + const result = await retry(func, { retryMinDelay, retries }); + + expect(result).toBe('success'); + expect(func).toHaveBeenCalledTimes(retries); + + const calls = delaySpy.mock.calls.map(([ms]) => ms); + expect(Math.min(...calls)).toBeGreaterThanOrEqual(retryMinDelay); + }); + it('should retry with the specified delay between attempts', async () => { const func = vi.fn().mockRejectedValueOnce(new Error('failure')).mockResolvedValue('success'); const retryMinDelay = 100; @@ -35,11 +50,10 @@ describe('retry', () => { expect(result).toBe('success'); expect(func).toHaveBeenCalledTimes(retries); + expect(delaySpy).to; const calls = delaySpy.mock.calls.map(([ms]) => ms); - calls.forEach(delayTime => { - expect(delayTime).toBeGreaterThanOrEqual(retryMinDelay); - expect(delayTime).toBeLessThanOrEqual(retryMaxDelay); - }); + expect(Math.min(...calls)).toBeGreaterThanOrEqual(retryMinDelay); + expect(Math.max(...calls)).toBeLessThanOrEqual(retryMaxDelay); }); it('should throw an error after the specified number of retries', async () => { @@ -65,10 +79,8 @@ describe('retry', () => { expect(result).toBe('success'); const calls = delaySpy.mock.calls.map(([ms]) => ms); - calls.forEach(delayTime => { - expect(delayTime).toBeGreaterThanOrEqual(retryMinDelay); - expect(delayTime).toBeLessThanOrEqual(retryMaxDelay); - }); + expect(Math.min(...calls)).toBeGreaterThanOrEqual(retryMinDelay); + expect(Math.max(...calls)).toBeLessThanOrEqual(retryMaxDelay); }); it('should increase delay exponentially based on the factor', async () => { @@ -82,9 +94,9 @@ describe('retry', () => { await retry(func, { retryMinDelay, factor, retries }).catch(() => {}); expect(func).toHaveBeenCalledTimes(3); - expect(delaySpy).toHaveBeenCalledWith(100); - expect(delaySpy).toHaveBeenCalledWith(200); - expect(delaySpy).toHaveBeenCalledWith(400); + expect(delaySpy).toHaveBeenCalledWith(retryMinDelay * Math.pow(factor, 0)); + expect(delaySpy).toHaveBeenCalledWith(retryMinDelay * Math.pow(factor, 1)); + expect(delaySpy).toHaveBeenCalledWith(retryMinDelay * Math.pow(factor, 2)); }); it('should abort the retry operation if the signal is already aborted', async () => { diff --git a/src/function/retry.ts b/src/function/retry.ts index e3952cc24..cc49d5587 100644 --- a/src/function/retry.ts +++ b/src/function/retry.ts @@ -16,7 +16,7 @@ interface RetryOptions { /** * The exponential factor to use for increasing retry delay. - * @default 2 + * @default 1 */ factor?: number; @@ -41,7 +41,7 @@ interface RetryOptions { const DEFAULT_RETRIES = Number.POSITIVE_INFINITY; const DEFAULT_MIN_DELAY = 0; const DEFAULT_MAX_DELAY = Number.POSITIVE_INFINITY; -const DEFAULT_FACTOR = 2; +const DEFAULT_FACTOR = 1; const DEFAULT_RANDOMIZE = false; /** @@ -80,7 +80,7 @@ export async function retry(func: () => Promise, retries: number): Promise * @param {number} [options.retries=Infinity] - The number of retries to attempt. Default is Infinity. * @param {number} [options.retryMinDelay=0] - The minimum delay in milliseconds between retry attempts. Default is 0. * @param {number} [options.retryMaxDelay=Infinity] - The maximum delay in milliseconds between retry attempts. Default is Infinity. - * @param {number} [options.factor=2] - The exponential factor to use for increasing retry delay. Default is 2. + * @param {number} [options.factor=1] - The exponential factor to use for increasing retry delay. Default is 1. * @param {boolean} [options.randomize=false] - Whether to randomize the retry delay by a factor between 1 and 2. Default is false. * @param {AbortSignal} [options.signal] - An AbortSignal to cancel the retry operation. * @returns {Promise} A promise that resolves with the value of the successful function call. From ec2c61be55c737c3b9559a11e3a53b47aaf815d2 Mon Sep 17 00:00:00 2001 From: kim-sung-jee Date: Mon, 20 Jan 2025 15:16:01 +0900 Subject: [PATCH 4/4] feat(retry-backoff-jitter): fix docs --- docs/ja/reference/function/retry.md | 43 +++++++++++++----- docs/ko/reference/function/retry.md | 30 ++++++++++-- docs/reference/function/retry.md | 36 +++++++++++---- docs/zh_hans/reference/function/retry.md | 58 ++++++++++++++++-------- 4 files changed, 125 insertions(+), 42 deletions(-) diff --git a/docs/ja/reference/function/retry.md b/docs/ja/reference/function/retry.md index 132effc30..98cc3cdaa 100644 --- a/docs/ja/reference/function/retry.md +++ b/docs/ja/reference/function/retry.md @@ -12,10 +12,13 @@ function retry(func: () => Promise, { retries, delay, signal }: RetryOptio ### パラメータ -- `func` (`() => Promise`): `Promise`を返す関数。 -- `retries`: 再試行する回数。デフォルトは `Number.POSITIVE_INFINITY` で、成功するまで再試行します。 -- `delay`: 再試行の間隔。単位はミリ秒(ms)で、デフォルトは `0` です。 -- `signal`: 再試行をキャンセルするための `AbortSignal`。 +- `func` (`() => Promise`): `Promise`を返す関数です。 +- `retries`: 再試行回数。デフォルトは `Number.POSITIVE_INFINITY`で、成功するまで再試行します。 +- `retryMinDelay`: 再試行間隔の最小値(ms)。デフォルトは `0` です。 +- `retryMaxDelay`: 再試行間隔の最大値(ms)。デフォルトは `Infinity` です。 +- `factor`: 再試行間隔を調整する係数。デフォルトは `1` です。 +- `randomize`: 再試行間隔に`factor`に基づくランダム要素を加えるかどうか。デフォルトは `false` です。 +- `signal`: 再試行をキャンセルするための `AbortSignal` です。 ### 戻り値 @@ -28,21 +31,39 @@ function retry(func: () => Promise, { retries, delay, signal }: RetryOptio ## 例 ```typescript -// `fetchData`が成功するまで無限に再試行します。 +// `fetchData` が成功するまで無限に再試行します。 const data1 = await retry(() => fetchData()); console.log(data1); -// `fetchData`が成功するまで3回だけ再試行します。 +// `fetchData` が成功するまで最大3回再試行します。 const data2 = await retry(() => fetchData(), 3); console.log(data2); -// `fetchData`が成功するまで3回だけ再試行し、間に100msの間隔があります。 -const data3 = await retry(() => fetchData(), { retries: 3, delay: 100 }); +// `fetchData` が成功するまで最大3回再試行し、再試行間隔を固定で100msに設定します。 +const data3 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100 }); console.log(data3); +// 再試行間の間隔が2倍ずつ増加します (100ms, 200ms, 400ms)。 +const data4 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100, factor: 2 }); +console.log(data4); + +// 指数バックオフとランダム化された再試行間隔 (100〜200ms) の間で再試行します。 +// 各再試行の間隔は以下のように計算されます: +// 1. 基本間隔は `retryMinDelay * factor^attempt` によって増加します。 +// 2. 計算された間隔は `retryMaxDelay` によって制限されます。 +// 3. `randomize` が true の場合、間隔が 1〜2倍の範囲でランダム化されます。 +// +// 例: +// - 最初の再試行: 100〜200ms の範囲 +// - 2回目の再試行: 200〜400ms の範囲 +// - 3回目の再試行: 400〜800ms の範囲 +const data5 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100, retryMaxDelay: 1000, randomize: true }); +console.log(data5); + const controller = new AbortController(); -// `fetchData`の再試行作業を`signal`でキャンセルできます。 -const data4 = await retry(() => fetchData(), { signal: controller.signal }); -console.log(data4); +// `signal` を使用して再試行を中止することができます。 +const data6 = await retry(() => fetchData(), { signal: controller.signal }); +console.log(data6); + ``` diff --git a/docs/ko/reference/function/retry.md b/docs/ko/reference/function/retry.md index 3ab39178d..f58e509c4 100644 --- a/docs/ko/reference/function/retry.md +++ b/docs/ko/reference/function/retry.md @@ -14,7 +14,10 @@ function retry(func: () => Promise, { retries, delay, signal }: RetryOptio - `func` (`() => Promise`): `Promise`를 반환하는 함수. - `retries`: 재시도할 횟수. 기본값은 `Number.POSITIVE_INFINITY`로, 성공할 때까지 재시도해요. -- `delay`: 재시도 사이 간격. 단위는 밀리세컨드(ms)로, 기본값은 `0`이에요. +- `retryMinDelay`: 재시도 사이 간격의 최소값. 단위는 밀리세컨드(ms)로, 기본값은 `0`이에요. +- `retryMaxDelay`: 재시도 사이 간격의 최대값. 단위는 밀리세컨드(ms)로, 기본값은 `Infinity`에요. +- `factor`: 재시도 사이 간격을 조절하는 계수. 기본값은 `1`이에요. +- `randomize`: `factor`에 더해 랜덤 요소를 추가할지 여부. 기본값은 `false`에요. - `signal`: 재시도를 취소할 수 있는 `AbortSignal`. ### 반환 값 @@ -36,13 +39,30 @@ console.log(data1); const data2 = await retry(() => fetchData(), 3); console.log(data2); -// `fetchData`가 성공할 때까지 3번만 재시도하고, 중간에 100ms씩 간격이 있어요. -const data3 = await retry(() => fetchData(), { retries: 3, delay: 100 }); +// `fetchData`가 성공할 때까지 3번만 재시도하고, 100ms의 고정된 지연시간으로 재시도해요. +const data3 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100 }); console.log(data3); +// 재시도 간 지연 시간이 2배씩 증가합니다 (100ms, 200ms, 400ms). +const data4 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100, factor: 2 }); +console.log(data4); + +// 지수 백오프와 랜덤화된 지연 시간 (100~200ms) 사이에서 재시도합니다. +// 각 재시도의 지연 시간은 아래와 같이 계산됩니다: +// 1. 기본 지연 시간은 `retryMinDelay * factor^attempt`로 증가합니다. +// 2. 계산된 지연 시간은 `retryMaxDelay`로 제한됩니다. +// 3. `randomize`가 true이므로, 지연 시간이 1~2배 사이로 랜덤화됩니다. +// +// 예: +// - 첫 번째 재시도: 100~200ms 범위 +// - 두 번째 재시도: 200~400ms 범위 +// - 세 번째 재시도: 400~800ms 범위 +const data5 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100, retryMaxDelay: 1000, randomize: true }); +console.log(data5); + const controller = new AbortController(); // `fetchData`를 재시도하는 작업을 `signal`로 취소할 수 있어요. -const data4 = await retry(() => fetchData(), { signal: controller.signal }); -console.log(data4); +const data6 = await retry(() => fetchData(), { signal: controller.signal }); +console.log(data6); ``` diff --git a/docs/reference/function/retry.md b/docs/reference/function/retry.md index 93b9e6548..805380e18 100644 --- a/docs/reference/function/retry.md +++ b/docs/reference/function/retry.md @@ -14,7 +14,10 @@ function retry(func: () => Promise, { retries, delay, signal }: RetryOptio - `func` (`() => Promise`): A function that returns a `Promise`. - `retries`: The number of times to retry. The default is `Number.POSITIVE_INFINITY`, which means it will retry until it succeeds. -- `delay`: The interval between retries, measured in milliseconds (ms). The default is `0`. +- `retryMinDelay`: The minimum delay between retries, measured in milliseconds (ms). The default is `0`. +- `retryMaxDelay`: The maximum delay between retries, measured in milliseconds (ms). The default is `Infinity`. +- `factor`: A factor to adjust the retry interval. The default is `1`. +- `randomize`: Whether to add a random factor to the delay based on the factor. The default is `false`. - `signal`: An `AbortSignal` that can be used to cancel the retries. ### Returns @@ -28,21 +31,38 @@ An error occurs when the number of retries reaches `retries` or when canceled by ## Examples ```typescript -// Retry indefinitely until `fetchData` succeeds. +// Retries indefinitely until `fetchData` succeeds. const data1 = await retry(() => fetchData()); console.log(data1); -// Retry only 3 times until `fetchData` succeeds. +// Retries up to 3 times until `fetchData` succeeds. const data2 = await retry(() => fetchData(), 3); console.log(data2); -// Retry only 3 times until `fetchData` succeeds, with a 100ms interval in between. -const data3 = await retry(() => fetchData(), { retries: 3, delay: 100 }); +// Retries up to 3 times with a fixed delay of 100ms between retries. +const data3 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100 }); console.log(data3); +// Retries with an exponentially increasing delay (100ms, 200ms, 400ms). +const data4 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100, factor: 2 }); +console.log(data4); + +// Retries with exponential backoff and random delay (100~200ms range). +// The delay for each retry is calculated as follows: +// 1. The base delay is `retryMinDelay * factor^attempt`. +// 2. The calculated delay is capped at `retryMaxDelay`. +// 3. If `randomize` is true, the delay is randomized within a 1~2x range. +// +// Example: +// - First retry: 100~200ms +// - Second retry: 200~400ms +// - Third retry: 400~800ms +const data5 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100, retryMaxDelay: 1000, randomize: true }); +console.log(data5); + const controller = new AbortController(); -// The retry operation for `fetchData` can be canceled with the `signal`. -const data4 = await retry(() => fetchData(), { signal: controller.signal }); -console.log(data4); +// The retry operation can be canceled using an `AbortSignal`. +const data6 = await retry(() => fetchData(), { signal: controller.signal }); +console.log(data6); ``` diff --git a/docs/zh_hans/reference/function/retry.md b/docs/zh_hans/reference/function/retry.md index 2f5023015..98b30b377 100644 --- a/docs/zh_hans/reference/function/retry.md +++ b/docs/zh_hans/reference/function/retry.md @@ -13,8 +13,11 @@ function retry(func: () => Promise, { retries, delay, signal }: RetryOptio ### 参数 - `func` (`() => Promise`): 一个返回 `Promise` 的函数。 -- `retries`: 重试的次数。默认值为 `Number.POSITIVE_INFINITY`,即会一直重试直到成功。 -- `delay`: 重试之间的间隔,单位为毫秒(ms),默认值为 `0`。 +- `retries`: 重试次数。默认值为 `Number.POSITIVE_INFINITY`,表示会一直重试直到成功。 +- `retryMinDelay`: 两次重试之间的最小间隔(以毫秒为单位)。默认值为 `0`。 +- `retryMaxDelay`: 两次重试之间的最大间隔(以毫秒为单位)。默认值为 `Infinity`。 +- `factor`: 用于调整重试间隔的倍数因子。默认值为 `1`。 +- `randomize`: 是否在 `factor` 的基础上增加随机延迟。默认值为 `false`。 - `signal`: 一个可以用来取消重试的 `AbortSignal`。 ### 返回值 @@ -28,20 +31,39 @@ function retry(func: () => Promise, { retries, delay, signal }: RetryOptio ## 示例 ```typescript -async function getNumber() { - return Promise.resolve(3); -} -async function getError() { - return Promise.reject(new Error('MyFailed')); -} -// 将返回 3 -await retry(getNumber, { - intervalMs: 1000, - retries: 2, -}); -// 将抛出异常 -await retry(getError, { - intervalMs: 1000, - retries: 2, -}); +// 无限重试,直到 `fetchData` 成功。 +const data1 = await retry(() => fetchData()); +console.log(data1); + +// 最多重试3次,直到 `fetchData` 成功。 +const data2 = await retry(() => fetchData(), 3); +console.log(data2); + +// 最多重试3次,每次重试间隔固定为100毫秒。 +const data3 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100 }); +console.log(data3); + +// 重试间隔按照指数倍数递增(100ms, 200ms, 400ms)。 +const data4 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100, factor: 2 }); +console.log(data4); + +// 带指数递增和随机化的重试间隔(100~200ms)。 +// 每次重试的间隔计算如下: +// 1. 基本间隔为 `retryMinDelay * factor^attempt`。 +// 2. 计算出的间隔被限制在 `retryMaxDelay` 以内。 +// 3. 如果 `randomize` 为 true,间隔会在 1~2 倍之间随机化。 +// +// 示例: +// - 第一次重试: 100~200ms +// - 第二次重试: 200~400ms +// - 第三次重试: 400~800ms +const data5 = await retry(() => fetchData(), { retries: 3, retryMinDelay: 100, retryMaxDelay: 1000, randomize: true }); +console.log(data5); + +const controller = new AbortController(); + +// 可以使用 `signal` 取消重试操作。 +const data6 = await retry(() => fetchData(), { signal: controller.signal }); +console.log(data6); + ```