功能说明
该示例实现带 full jitter 的退避重试。基础延迟按指数增长并以 capMs 封顶,随后在零到该延迟之间均匀取随机值,因此大量客户端不会在同一时刻再次重试。retryWithFullJitter 负责执行操作、在失败后等待并重试,直到成功或达到最大次数;等待过程可通过 AbortSignal 取消。适合调用外部 API、消息投递与连接重建。
关键代码
fullJitterDelay 校验尝试序号、基础值、上限与随机源,计算 exponential = min(cap, base * 2^attempt),再返回 floor(random * (exponential + 1))。加一是为了让区间包含上限附近的整数值。sleep 在每次调用时注册一次性 abort 监听,定时器与监听在完成或中止时统一清理;已经中止的信号会立即拒绝。重试循环保留最后一次错误,次数耗尽后原样抛出。
使用注意
full jitter 的期望等待约为上限的一半,比等抖或装饰抖动更激进,适合对重试同步敏感但能接受较大方差的场景。只有可重试错误才应进入循环,认证失败或参数错误应直接抛出。等待被中止时抛出的是 signal.reason,可能不是 Error,调用方需区分取消与业务失败。maxAttempts 包含首次执行,设为三表示最多尝试三次。
验证方式
固定随机源为 0.5,断言每次延迟为指数上限的一半左右并在 cap 处封顶;随机源返回负值或一应抛 RangeError;让操作前两次失败第三次成功,统计调用次数与传入的 attempt 序号;达到最大次数后断言抛出最后一次错误;在等待期间中止信号,确认 Promise 以中止原因拒绝且没有继续重试。
代码示例
export interface BackoffOptions {
baseMs?: number;
capMs?: number;
maxAttempts?: number;
random?: () => number;
signal?: AbortSignal;
}
function positive(value: number, name: string): void {
if (!Number.isFinite(value) || value <= 0) {
throw new RangeError(name + " must be a positive finite number");
}
}
export function fullJitterDelay(attempt: number, options: BackoffOptions = {}): number {
if (!Number.isInteger(attempt) || attempt < 0) {
throw new RangeError("attempt must be a non-negative integer");
}
const baseMs = options.baseMs ?? 100;
const capMs = options.capMs ?? 20_000;
const random = options.random ?? Math.random;
positive(baseMs, "baseMs");
positive(capMs, "capMs");
if (typeof random !== "function") throw new TypeError("random must be a function");
const exponential = Math.min(capMs, baseMs * 2 ** attempt);
const sample = random();
if (!(sample >= 0 && sample < 1)) {
throw new RangeError("random must return a value in [0, 1)");
}
return Math.floor(sample * (exponential + 1));
}
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
if (signal?.aborted) return Promise.reject(signal.reason);
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
cleanup();
resolve();
}, ms);
const onAbort = (): void => {
cleanup();
reject(signal?.reason ?? new Error("backoff aborted"));
};
function cleanup(): void {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
}
signal?.addEventListener("abort", onAbort, { once: true });
});
}
export async function retryWithFullJitter<T>(
operation: (attempt: number) => Promise<T>,
options: BackoffOptions = {},
): Promise<T> {
if (typeof operation !== "function") throw new TypeError("operation must be a function");
const maxAttempts = options.maxAttempts ?? 5;
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
throw new RangeError("maxAttempts must be a positive integer");
}
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
try {
return await operation(attempt);
} catch (error) {
lastError = error;
if (attempt === maxAttempts - 1) break;
const delay = fullJitterDelay(attempt, options);
await sleep(delay, options.signal);
}
}
throw lastError;
}