Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(retry-backoff-jitter): additional options exponential backof, jitter #940

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions docs/ja/reference/function/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ function retry<T>(func: () => Promise<T>, { retries, delay, signal }: RetryOptio

### パラメータ

- `func` (`() => Promise<T>`): `Promise`を返す関数。
- `retries`: 再試行する回数。デフォルトは `Number.POSITIVE_INFINITY` で、成功するまで再試行します。
- `delay`: 再試行の間隔。単位はミリ秒(ms)で、デフォルトは `0` です。
- `signal`: 再試行をキャンセルするための `AbortSignal`。
- `func` (`() => Promise<T>`): `Promise`を返す関数です。
- `retries`: 再試行回数。デフォルトは `Number.POSITIVE_INFINITY`で、成功するまで再試行します。
- `retryMinDelay`: 再試行間隔の最小値(ms)。デフォルトは `0` です。
- `retryMaxDelay`: 再試行間隔の最大値(ms)。デフォルトは `Infinity` です。
- `factor`: 再試行間隔を調整する係数。デフォルトは `1` です。
- `randomize`: 再試行間隔に`factor`に基づくランダム要素を加えるかどうか。デフォルトは `false` です。
- `signal`: 再試行をキャンセルするための `AbortSignal` です。

### 戻り値

Expand All @@ -28,21 +31,39 @@ function retry<T>(func: () => Promise<T>, { 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);

```
30 changes: 25 additions & 5 deletions docs/ko/reference/function/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ function retry<T>(func: () => Promise<T>, { retries, delay, signal }: RetryOptio

- `func` (`() => Promise<T>`): `Promise`를 반환하는 함수.
- `retries`: 재시도할 횟수. 기본값은 `Number.POSITIVE_INFINITY`로, 성공할 때까지 재시도해요.
- `delay`: 재시도 사이 간격. 단위는 밀리세컨드(ms)로, 기본값은 `0`이에요.
- `retryMinDelay`: 재시도 사이 간격의 최소값. 단위는 밀리세컨드(ms)로, 기본값은 `0`이에요.
- `retryMaxDelay`: 재시도 사이 간격의 최대값. 단위는 밀리세컨드(ms)로, 기본값은 `Infinity`에요.
- `factor`: 재시도 사이 간격을 조절하는 계수. 기본값은 `1`이에요.
- `randomize`: `factor`에 더해 랜덤 요소를 추가할지 여부. 기본값은 `false`에요.
- `signal`: 재시도를 취소할 수 있는 `AbortSignal`.

### 반환 값
Expand All @@ -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);
```
36 changes: 28 additions & 8 deletions docs/reference/function/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ function retry<T>(func: () => Promise<T>, { retries, delay, signal }: RetryOptio

- `func` (`() => Promise<T>`): 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
Expand All @@ -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);
```
58 changes: 40 additions & 18 deletions docs/zh_hans/reference/function/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ function retry<T>(func: () => Promise<T>, { retries, delay, signal }: RetryOptio
### 参数

- `func` (`() => Promise<T>`): 一个返回 `Promise` 的函数。
- `retries`: 重试的次数。默认值为 `Number.POSITIVE_INFINITY`,即会一直重试直到成功。
- `delay`: 重试之间的间隔,单位为毫秒(ms),默认值为 `0`。
- `retries`: 重试次数。默认值为 `Number.POSITIVE_INFINITY`,表示会一直重试直到成功。
- `retryMinDelay`: 两次重试之间的最小间隔(以毫秒为单位)。默认值为 `0`。
- `retryMaxDelay`: 两次重试之间的最大间隔(以毫秒为单位)。默认值为 `Infinity`。
- `factor`: 用于调整重试间隔的倍数因子。默认值为 `1`。
- `randomize`: 是否在 `factor` 的基础上增加随机延迟。默认值为 `false`。
- `signal`: 一个可以用来取消重试的 `AbortSignal`。

### 返回值
Expand All @@ -28,20 +31,39 @@ function retry<T>(func: () => Promise<T>, { 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);

```
80 changes: 71 additions & 9 deletions src/function/retry.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -15,26 +16,87 @@ 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 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 delay = 100;
const start = Date.now();
const result = await retry(func, { delay, retries: 2 });
const end = Date.now();
const retryMinDelay = 100;
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(delay);
expect(func).toHaveBeenCalledTimes(retries);

expect(delaySpy).to;
const calls = delaySpy.mock.calls.map(([ms]) => ms);
expect(Math.min(...calls)).toBeGreaterThanOrEqual(retryMinDelay);
expect(Math.max(...calls)).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);
expect(Math.min(...calls)).toBeGreaterThanOrEqual(retryMinDelay);
expect(Math.max(...calls)).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(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 () => {
Expand Down
Loading
Loading