功能说明
该示例实现对冲请求:先发起第一次尝试,若在 delayMs 内没有结果,再并行发起第二次,谁先成功就采用谁并取消其余尝试。只有当所有尝试都失败时才拒绝,并使用 AggregateError 保留每个错误。适用于对尾延迟敏感的只读接口,通过少量额外请求换取更稳定的响应时间。
关键代码
每个尝试拥有独立的 AbortController,统一收集到 Set 中,任一尝试成功或外部取消时在 cleanup 中全部中止,避免后台请求继续消耗资源。启动过程由 setTimeout 串联,定时器也集中清理。成功回调通过 finish 保证只结算一次;失败回调累计 settled,只有已启动数量达到 maxAttempts 且全部失败时才聚合拒绝。外部信号在入口先检查,并在 Promise 内注册一次性监听。
使用注意
对冲只适合幂等请求,写操作重复执行可能造成副作用。启动第二个请求会增加下游负载,delayMs 应基于目标接口的延迟分位数设置,maxAttempts 不宜过大。取消是协作式的,attempt 必须把 AbortSignal 传给 fetch 等可取消 API,否则落败请求仍会继续执行。AggregateError 在旧运行时可能不存在,需要兼容时应改为自定义错误。
验证方式
用可控 Promise 模拟尝试:第一次永不解决,第二次成功后断言整体成功且第一次的 signal 被中止;让两次都失败,断言收到 AggregateError 且包含两个错误;外部信号在启动前已中止时立即拒绝;在延迟期间中止,断言不会启动第二次;测试负 delayMs 与非整数 maxAttempts 抛 RangeError。
代码示例
export interface HedgedRequestOptions {
delayMs: number;
maxAttempts?: number;
signal?: AbortSignal;
}
export async function hedgedRequest<T>(
attempt: (signal: AbortSignal, attemptIndex: number) => Promise<T>,
options: HedgedRequestOptions,
): Promise<T> {
if (typeof attempt !== "function") throw new TypeError("attempt must be a function");
if (!Number.isFinite(options.delayMs) || options.delayMs < 0) {
throw new RangeError("delayMs must be a non-negative finite number");
}
const maxAttempts = options.maxAttempts ?? 2;
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
throw new RangeError("maxAttempts must be a positive integer");
}
const external = options.signal;
if (external?.aborted) throw external.reason;
return new Promise<T>((resolve, reject) => {
const controllers = new Set<AbortController>();
const timers = new Set<ReturnType<typeof setTimeout>>();
const failures: unknown[] = [];
let launched = 0;
let settled = 0;
let finished = false;
const cleanup = (): void => {
for (const timer of timers) clearTimeout(timer);
timers.clear();
external?.removeEventListener("abort", onExternalAbort);
for (const controller of controllers) controller.abort();
controllers.clear();
};
const finish = (callback: () => void): void => {
if (finished) return;
finished = true;
cleanup();
callback();
};
const onExternalAbort = (): void => {
finish(() => reject(external?.reason ?? new Error("hedged request aborted")));
};
const launch = (): void => {
if (finished || launched >= maxAttempts) return;
const index = launched;
launched += 1;
const controller = new AbortController();
controllers.add(controller);
if (external?.aborted) controller.abort(external.reason);
Promise.resolve()
.then(() => attempt(controller.signal, index))
.then(
(value) => finish(() => resolve(value)),
(error) => {
if (finished) return;
failures.push(error);
settled += 1;
if (settled === launched && launched === maxAttempts) {
finish(() => reject(new AggregateError(failures, "all hedged attempts failed")));
}
},
);
if (index + 1 < maxAttempts) {
const timer = setTimeout(() => {
timers.delete(timer);
launch();
}, options.delayMs);
timers.add(timer);
}
};
external?.addEventListener("abort", onExternalAbort, { once: true });
launch();
});
}