diff --git a/docs/ja/reference/promise/addJitter.md b/docs/ja/reference/promise/addJitter.md new file mode 100644 index 000000000..671a75063 --- /dev/null +++ b/docs/ja/reference/promise/addJitter.md @@ -0,0 +1,98 @@ +# addJitter + +基準となる遅延時間にランダムなジッター(jitter)を加えて、多数のタスクが同じタイミングで一斉に実行される("thundering herd" 問題)ことを避けます。 + +戻り値は次の区間から一様分布でサンプリングされます: + +``` +[delay - delay * factor, delay + delay * factor] +``` + +その後、負の値にならないよう 0 未満なら 0 に丸められます。 + +再試行(リトライ)/指数バックオフ、ポーリング間隔の分散、同時起動の集中緩和などに有用です。 + +## インターフェース + +```typescript +function addJitter(delay: number, factor?: number, rng?: () => number): number; +``` + +### パラメータ + +- `delay` (`number`): 基準となる遅延(ミリ秒)。 +- `factor` (`number`, 省略可, 既定値 `0.2`): ジッター係数(推奨範囲 `[0, 1]`)。最大で±何割ブレるかを示し、`0` ならジッターなし。 +- `rng` (`() => number`, 省略可, 既定値 `Math.random`): `[0, 1)` の乱数を返す関数。テストで再現性を得るために差し替え可能。 + +### 戻り値 + +(`number`): ジッター適用後の 0 以上の遅延時間(ミリ秒)。 + +## 例 + +### 基本的な使用法 + +```typescript +import { addJitter } from 'es-toolkit/promise'; + +const base = 1000; // 1秒 +const value = addJitter(base); // factor=0.2 の場合 [800, 1200] の範囲 +console.log(value); +``` + +### ジッター係数を指定 + +```typescript +addJitter(500, 0.5); // [250, 750] の範囲 +``` + +### カスタム RNG(決定的テスト) + +```typescript +// rng が常に 1 を返す -> 上限値 +const upper = addJitter(100, 0.3, () => 1); // 130 +// rng が常に 0 を返す -> 下限値 +const lower = addJitter(100, 0.3, () => 0); // 70 +``` + +### 指数バックオフとの併用 + +```typescript +import { addJitter, delay } from 'es-toolkit/promise'; + +async function fetchWithRetry(run: () => Promise) { + let base = 500; + for (let attempt = 0; attempt < 5; attempt++) { + try { + return await run(); + } catch (err) { + const wait = addJitter(base); // 同時リトライを分散 + await delay(wait); + base *= 2; // 指数的に増加 + } + } + throw new Error('All retries failed'); +} +``` + +### ポーリング間隔を分散 + +```typescript +import { addJitter, delay } from 'es-toolkit/promise'; + +async function poll(run: () => Promise) { + const base = 2000; + const wait = addJitter(base, 0.15); + await delay(wait); + await run(); + // 次回をスケジュール + setTimeout(() => poll(run), addJitter(base, 0.15)); +} +``` + +## 動作と注意事項 + +- 区間全体で一様分布。 +- `factor > 1`(非推奨)や `delay` が極端に小さくても負値にはならず 0 に切り上げ。 +- `factor = 0` なら元の `delay` をそのまま返す。 +- 再現テストのためカスタム `rng` を差し替え可能。 diff --git a/docs/ko/reference/promise/addJitter.md b/docs/ko/reference/promise/addJitter.md new file mode 100644 index 000000000..22873627e --- /dev/null +++ b/docs/ko/reference/promise/addJitter.md @@ -0,0 +1,98 @@ +# addJitter + +기본 지연 시간에 작은 무작위 흔들림(jitter)을 더해서 여러 작업이 동시에 몰려 실행되는 "우르르 현상(thundering herd)"을 줄여줘요. + +반환 값은 아래 구간에서 균등 분포로 하나를 뽑아요: + +``` +[delay - delay * factor, delay + delay * factor] +``` + +그리고 음수가 되지 않도록 0보다 작으면 0으로 고정돼요. + +주로 재시도(backoff) 로직, 폴링 주기 분산, 동시에 여러 인스턴스가 같은 타이밍에 호출하는 것을 퍼뜨리고 싶을 때 사용해요. + +## 인터페이스 + +```typescript +function addJitter(delay: number, factor?: number, rng?: () => number): number; +``` + +### 파라미터 + +- `delay` (`number`): 기본 지연 시간(밀리세컨드). +- `factor` (`number`, 선택, 기본값 `0.2`): 지터 비율(권장 범위 `[0, 1]`). 최대 ± 퍼센트를 의미하고 0이면 지터가 없어요. +- `rng` (`() => number`, 선택, 기본값 `Math.random`): `[0, 1)` 범위 부동소수 값을 돌려주는 랜덤 함수. 테스트에서 재현성을 위해 직접 주입할 수 있어요. + +### 반환 값 + +(`number`): 지터가 적용된 (0 이상) 지연 시간 값. + +## 예시 + +### 기본 사용법 + +```typescript +import { addJitter } from 'es-toolkit/promise'; + +const base = 1000; // 1초 +const value = addJitter(base); // factor=0.2면 [800, 1200] 범위 +console.log(value); +``` + +### 지터 비율 직접 지정 + +```typescript +addJitter(500, 0.5); // [250, 750] 범위 +``` + +### 커스텀 RNG 주입 (결과 고정) + +```typescript +// rng가 항상 1을 반환 -> 상한값 +const upper = addJitter(100, 0.3, () => 1); // 130 +// rng가 항상 0을 반환 -> 하한값 +const lower = addJitter(100, 0.3, () => 0); // 70 +``` + +### 지수형 백오프와 함께 사용 + +```typescript +import { addJitter, delay } from 'es-toolkit/promise'; + +async function fetchWithRetry(run: () => Promise) { + let base = 500; + for (let attempt = 0; attempt < 5; attempt++) { + try { + return await run(); + } catch (err) { + const wait = addJitter(base); // 동시에 몰리는 재시도를 분산 + await delay(wait); + base *= 2; // 지수 증가 + } + } + throw new Error('All retries failed'); +} +``` + +### 폴링 주기 분산 + +```typescript +import { addJitter, delay } from 'es-toolkit/promise'; + +async function poll(run: () => Promise) { + const base = 2000; + const wait = addJitter(base, 0.15); + await delay(wait); + await run(); + // 다음 실행 예약 + setTimeout(() => poll(run), addJitter(base, 0.15)); +} +``` + +## 동작 & 참고 + +- 구간 전체에서 균등 분포로 선택돼요. +- `factor > 1` (권장하지 않음) 이거나 `delay`가 아주 작아도 음수가 되면 0으로 고정돼요. +- `factor = 0`이면 지터 없이 원래 값이 반환돼요. +- 테스트 재현을 위해 커스텀 `rng`를 주입할 수 있어요. diff --git a/docs/reference/promise/addJitter.md b/docs/reference/promise/addJitter.md new file mode 100644 index 000000000..c961970c1 --- /dev/null +++ b/docs/reference/promise/addJitter.md @@ -0,0 +1,97 @@ +# addJitter + +Adds randomized jitter to a base delay duration to avoid synchronized bursts (the "thundering herd" problem). + +The returned value is sampled uniformly from the interval: + +``` +[delay - delay * factor, delay + delay * factor] +``` + +and then clamped to be non‑negative (never returns a negative delay). + +Use this to slightly randomize retry or polling intervals so that many clients do not all fire at exactly the same moment. + +## Signature + +```typescript +function addJitter(delay: number, factor?: number, rng?: () => number): number; +``` + +### Parameters + +- `delay` (`number`): The base delay in milliseconds. +- `factor` (`number`, optional, default = `0.2`): The jitter factor in the range `[0, 1]` describing the maximum +/- proportion applied. `0` means no jitter. +- `rng` (`() => number`, optional, default = `Math.random`): A random number generator that returns a float in `[0, 1)`. Provide your own for deterministic testing. + +### Returns + +(`number`): A non‑negative delay with jitter applied. + +## Examples + +### Basic Usage + +```typescript +import { addJitter } from 'es-toolkit/promise'; + +const base = 1000; // 1s +const value = addJitter(base); // in [800, 1200] when factor = 0.2 (default) +console.log(value); +``` + +### Custom Factor + +```typescript +addJitter(500, 0.5); // in [250, 750] +``` + +### Deterministic (Inject RNG) + +```typescript +// Always returns the upper bound because rng() returns 1 +const upper = addJitter(100, 0.3, () => 1); // 130 +// Always returns the lower bound because rng() returns 0 +const lower = addJitter(100, 0.3, () => 0); // 70 +``` + +### With Exponential Backoff + +```typescript +import { addJitter } from 'es-toolkit/promise'; +import { delay } from 'es-toolkit/promise'; + +async function fetchWithRetry(run: () => Promise) { + let base = 500; + for (let attempt = 0; attempt < 5; attempt++) { + try { + return await run(); + } catch (err) { + const wait = addJitter(base); // spread concurrent retries + await delay(wait); + base *= 2; // exponential growth + } + } + throw new Error('All retries failed'); +} +``` + +### Periodic Polling + +```typescript +async function poll(run: () => Promise) { + const base = 2000; + const wait = addJitter(base, 0.15); + await delay(wait); + await run(); + // schedule next + setTimeout(() => poll(run), addJitter(base, 0.15)); +} +``` + +## Behavior and Notes + +- Uniform distribution across the interval. +- Clamped at 0: if `factor > 1` (not recommended) or `delay` is very small, the computed value will never go below 0. +- Setting `factor` to `0` returns the original `delay`. +- Passing a custom `rng` enables reproducible tests. diff --git a/docs/zh_hans/reference/promise/addJitter.md b/docs/zh_hans/reference/promise/addJitter.md new file mode 100644 index 000000000..c9e79e1a1 --- /dev/null +++ b/docs/zh_hans/reference/promise/addJitter.md @@ -0,0 +1,98 @@ +# addJitter + +为基础延迟时间添加随机抖动(jitter),以避免大量任务在同一时间点同时触发(“惊群”/thundering herd 问题)。 + +返回值在如下区间内等概率采样: + +``` +[delay - delay * factor, delay + delay * factor] +``` + +随后会被限制为不小于 0(不会返回负值)。 + +适用于:重试 / 指数退避、轮询调度、分散批量任务启动时间等需要减少竞争与瞬时压力的场景。 + +## 签名 + +```typescript +function addJitter(delay: number, factor?: number, rng?: () => number): number; +``` + +### 参数 + +- `delay` (`number`): 基础延迟(毫秒)。 +- `factor` (`number`, 可选,默认 `0.2`): 抖动系数,取值范围建议在 `[0, 1]` 之间,表示最大正负偏移比例;`0` 表示无抖动。 +- `rng` (`() => number`, 可选,默认 `Math.random`): 返回 `[0, 1)` 区间浮点数的随机数生成函数,可注入自定义实现以便测试复现。 + +### 返回值 + +(`number`): 应用抖动后的非负延迟时间(毫秒)。 + +## 示例 + +### 基本用法 + +```typescript +import { addJitter } from 'es-toolkit/promise'; + +const base = 1000; // 1 秒 +const value = addJitter(base); // 当 factor = 0.2 时,返回值位于 [800, 1200] +console.log(value); +``` + +### 自定义抖动系数 + +```typescript +addJitter(500, 0.5); // 结果位于 [250, 750] +``` + +### 注入自定义 RNG(确定性测试) + +```typescript +// rng 始终返回 1 -> 取上界 +const upper = addJitter(100, 0.3, () => 1); // 130 +// rng 始终返回 0 -> 取下界 +const lower = addJitter(100, 0.3, () => 0); // 70 +``` + +### 与指数退避结合的重试策略 + +```typescript +import { addJitter, delay } from 'es-toolkit/promise'; + +async function fetchWithRetry(run: () => Promise) { + let base = 500; + for (let attempt = 0; attempt < 5; attempt++) { + try { + return await run(); + } catch (err) { + const wait = addJitter(base); // 分散并发重试 + await delay(wait); + base *= 2; // 指数增加 + } + } + throw new Error('All retries failed'); +} +``` + +### 用于轮询 + +```typescript +import { addJitter, delay } from 'es-toolkit/promise'; + +async function poll(run: () => Promise) { + const base = 2000; + const wait = addJitter(base, 0.15); + await delay(wait); + await run(); + // 调度下一次 + setTimeout(() => poll(run), addJitter(base, 0.15)); +} +``` + +## 行为与说明 + +- 区间内均匀分布(uniform)。 +- 永不返回负值:即使 `factor` 设置大于 1(不推荐)或 `delay` 很小,结果也会被下限截断为 0。 +- `factor = 0` 时直接返回原始 `delay`。 +- 可通过自定义 `rng` 进行可重复测试。 diff --git a/src/promise/addJitter.spec.ts b/src/promise/addJitter.spec.ts new file mode 100644 index 000000000..4b5235002 --- /dev/null +++ b/src/promise/addJitter.spec.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { addJitter } from './addJitter'; + +describe('addJitter', () => { + it('returns a number within the expected symmetric range', () => { + const delay = 1000; + const factor = 0.2; + for (let i = 0; i < 1000; i++) { + const value = addJitter(delay, factor); + expect(value).greaterThanOrEqual(delay - delay * factor); + expect(value).lessThanOrEqual(delay + delay * factor); + } + }); + + it('never returns a negative value even with large factor', () => { + for (let i = 0; i < 200; i++) { + const value = addJitter(10, 5); // range would be [-40, 60] before clamping + expect(value).greaterThanOrEqual(0); + } + }); + + it('falls back to Math.random when no rng provided', () => { + // We cannot easily assert randomness, but can call to ensure no throw and in range. + const value = addJitter(500, 0.1); + expect(value).greaterThanOrEqual(450); + expect(value).lessThanOrEqual(550); + }); + + it('uses custom rng for deterministic output', () => { + // rng that always returns 1 -> jitter = delay * factor * (2 * 1 - 1) = delay * factor + const valueMax = addJitter(100, 0.3, () => 1); + expect(valueMax).toBe(100 + 100 * 0.3); + + // rng that always returns 0 -> jitter = delay * factor * (2 * 0 - 1) = -delay * factor + const valueMin = addJitter(100, 0.3, () => 0); + expect(valueMin).toBe(100 - 100 * 0.3); + }); + + it('handles factor = 0 (no jitter)', () => { + for (let i = 0; i < 10; i++) { + expect(addJitter(123, 0)).toBe(123); + } + }); + + it('distribution is roughly centered around the base delay', () => { + // Statistical sanity check (not strict) to ensure mean is near delay. + const delay = 200; + const factor = 0.5; + const samples = 5000; + let sum = 0; + for (let i = 0; i < samples; i++) { + sum += addJitter(delay, factor); + } + const mean = sum / samples; + // Allow a tolerance of 2% of delay. + const tolerance = delay * 0.02; + expect(Math.abs(mean - delay)).lessThanOrEqual(tolerance); + }); +}); diff --git a/src/promise/addJitter.ts b/src/promise/addJitter.ts new file mode 100644 index 000000000..558c2e7fd --- /dev/null +++ b/src/promise/addJitter.ts @@ -0,0 +1,30 @@ +/** + * Adds randomized jitter to a base delay duration to avoid synchronized bursts ("thundering herd"). + * + * The returned value is sampled uniformly from the interval: + * [delay - delay * factor, delay + delay * factor], then clamped to be non‑negative. + * This is useful for retry / backoff strategies, scheduling polls, or any repeated + * task where spreading concurrent executions helps reduce contention. + * + * @param {number} delay - The base delay in milliseconds. + * @param {number} [factor=0.2] - The jitter factor (0–1) describing the maximum +/- proportion applied. + * @param {() => number} [rng=Math.random] - Optional RNG returning a float in [0, 1). Inject for deterministic tests. + * @returns {number} A (non‑negative) delay value with jitter applied. + * + * @example + * // Returns a value between 800 and 1200 when delay = 1000 and factor = 0.2 + * const jittered = addJitter(1000); + * + * @example + * // Using with exponential backoff + * let base = 500; + * for (let attempt = 0; attempt < 5; attempt++) { + * const wait = addJitter(base); + * await delay(wait); + * base *= 2; // exponential increase + * } + */ +export function addJitter(delay: number, factor = 0.2, rng: () => number = Math.random) { + const jitter = delay * factor * (rng() * 2 - 1); + return Math.max(0, delay + jitter); +} diff --git a/src/promise/index.ts b/src/promise/index.ts index e738f06d1..108f8b34a 100644 --- a/src/promise/index.ts +++ b/src/promise/index.ts @@ -1,3 +1,4 @@ +export { addJitter } from './addJitter.ts'; export { delay } from './delay.ts'; export { Mutex } from './mutex.ts'; export { Semaphore } from './semaphore.ts';