编码母题 / P0 / 异步编程 / 02
支持超时取消与重试的执行器
先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。
重要程度必练难度困难预计完成50–70 分钟
题目是什么
包装可能同步或异步的 operation,支持单次尝试超时、外部取消、条件重试和指数退避。取消不能再启动新尝试;超时只能协作通知底层操作。
输入输出示例
retries=2 最多执行 3 次;在退避期间取消,应立即停止而非等完整延迟。
练习起手式与验证
复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。
/*
* 题目:支持超时取消与重试的执行器
* 要求:包装可能同步或异步的 operation,支持单次尝试超时、外部取消、条件重试和指数退避。
* 取消不能再启动新尝试;超时只能协作通知底层操作。
* 示例:retries=2 最多执行 3 次;在退避期间取消,应立即停止而非等完整延迟。
*/
class TimeoutError extends Error {
constructor(message = "operation timed out") {
super(message);
this.name = "TimeoutError";
}
}
class AbortExecutionError extends Error {
constructor(message = "operation aborted") {
super(message);
this.name = "AbortError";
}
}
function abortReason(signal) {
return signal && signal.reason instanceof Error
? signal.reason
: new AbortExecutionError();
}
function abortableDelay(ms, signal) {
if (signal?.aborted) return Promise.reject(abortReason(signal));
return new Promise((resolve, reject) => {
const timer = setTimeout(done, ms);
function done() {
cleanup();
resolve();
}
function onAbort() {
cleanup();
reject(abortReason(signal));
}
function cleanup() {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
}
signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function runAttempt(operation, attempt, timeout, outerSignal) {
if (outerSignal?.aborted) throw abortReason(outerSignal);
const controller = new AbortController();
let timedOut = false;
const onOuterAbort = () => controller.abort(abortReason(outerSignal));
outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
const timer =
Number.isFinite(timeout) && timeout >= 0
? setTimeout(() => {
timedOut = true;
controller.abort(new TimeoutError());
}, timeout)
: null;
let onAttemptAbort;
try {
const operationPromise = Promise.resolve().then(() =>
operation(controller.signal, attempt),
);
const cancellationPromise = new Promise((_, reject) => {
onAttemptAbort = () => reject(controller.signal.reason);
controller.signal.addEventListener("abort", onAttemptAbort, {
once: true,
});
});
return await Promise.race([operationPromise, cancellationPromise]);
} catch (error) {
if (timedOut) throw new TimeoutError();
throw error;
} finally {
if (timer) clearTimeout(timer);
if (onAttemptAbort)
controller.signal.removeEventListener("abort", onAttemptAbort);
outerSignal?.removeEventListener("abort", onOuterAbort);
}
}
async function execute(operation, options = {}) {}
(async () => {
let calls = 0;
console.log(await execute(
() => {
calls += 1;
if (calls < 3) throw new Error("retry");
return 9;
},
{ retries: 2 },
)); // 预期:9
console.log(calls); // 预期:3
console.log(await Promise.resolve(execute(() => new Promise(() => {}), { timeout: 5 })).then(() => false, () => true)); // 预期:true(拒绝)
})().catch(console.error);
/*
* 题目:支持超时取消与重试的执行器
* 要求:包装可能同步或异步的 operation,支持单次尝试超时、外部取消、条件重试和指数退避。
* 取消不能再启动新尝试;超时只能协作通知底层操作。
* 示例:retries=2 最多执行 3 次;在退避期间取消,应立即停止而非等完整延迟。
*/
class TimeoutError extends Error {
constructor(message = "operation timed out") {
super(message);
this.name = "TimeoutError";
}
}
class AbortExecutionError extends Error {
constructor(message = "operation aborted") {
super(message);
this.name = "AbortError";
}
}
function abortReason(signal) {
return signal && signal.reason instanceof Error
? signal.reason
: new AbortExecutionError();
}
function abortableDelay(ms, signal) {
if (signal?.aborted) return Promise.reject(abortReason(signal));
return new Promise((resolve, reject) => {
const timer = setTimeout(done, ms);
function done() {
cleanup();
resolve();
}
function onAbort() {
cleanup();
reject(abortReason(signal));
}
function cleanup() {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
}
signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function runAttempt(operation, attempt, timeout, outerSignal) {
if (outerSignal?.aborted) throw abortReason(outerSignal);
const controller = new AbortController();
let timedOut = false;
const onOuterAbort = () => controller.abort(abortReason(outerSignal));
outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
const timer =
Number.isFinite(timeout) && timeout >= 0
? setTimeout(() => {
timedOut = true;
controller.abort(new TimeoutError());
}, timeout)
: null;
let onAttemptAbort;
try {
const operationPromise = Promise.resolve().then(() =>
operation(controller.signal, attempt),
);
const cancellationPromise = new Promise((_, reject) => {
onAttemptAbort = () => reject(controller.signal.reason);
controller.signal.addEventListener("abort", onAttemptAbort, {
once: true,
});
});
return await Promise.race([operationPromise, cancellationPromise]);
} catch (error) {
if (timedOut) throw new TimeoutError();
throw error;
} finally {
if (timer) clearTimeout(timer);
if (onAttemptAbort)
controller.signal.removeEventListener("abort", onAttemptAbort);
outerSignal?.removeEventListener("abort", onOuterAbort);
}
}
async function execute(operation, options = {}) {}
(async () => {
let calls = 0;
console.log(await execute(
() => {
calls += 1;
if (calls < 3) throw new Error("retry");
return 9;
},
{ retries: 2 },
)); // 预期:9
console.log(calls); // 预期:3
console.log(await Promise.resolve(execute(() => new Promise(() => {}), { timeout: 5 })).then(() => false, () => true)); // 预期:true(拒绝)
})().catch(console.error);解题思路
展开思路、不变量与常见边界
关键切入点:每轮独立 AbortController 合并外部取消与本轮计时器;退避和清理也需响应取消。
核心模型:每次尝试创建局部 AbortController,把外部取消和超时汇入局部信号; finally 清理定时器和监听器。退避也必须可取消。delay = baseDelay * 2^retryIndex。 不变量:最多执行 retries + 1 次;取消后不再开始新尝试;每次尝试结束后无监听 器和定时器残留;只由 shouldRetry 判定的错误进入下一轮。 常见错误:用 Promise.race 超时却不通知底层任务;把总次数与重试次数混淆; 重试非幂等副作用;取消期间仍等待完整退避时间。
参考代码与验证
“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。
class TimeoutError extends Error {
constructor(message = "operation timed out") {
super(message);
this.name = "TimeoutError";
}
}
class AbortExecutionError extends Error {
constructor(message = "operation aborted") {
super(message);
this.name = "AbortError";
}
}
function abortReason(signal) {
return signal && signal.reason instanceof Error
? signal.reason
: new AbortExecutionError();
}
function abortableDelay(ms, signal) {
if (signal?.aborted) return Promise.reject(abortReason(signal));
return new Promise((resolve, reject) => {
const timer = setTimeout(done, ms);
function done() {
cleanup();
resolve();
}
function onAbort() {
cleanup();
reject(abortReason(signal));
}
function cleanup() {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
}
signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function runAttempt(operation, attempt, timeout, outerSignal) {
if (outerSignal?.aborted) throw abortReason(outerSignal);
const controller = new AbortController();
let timedOut = false;
const onOuterAbort = () => controller.abort(abortReason(outerSignal));
outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
const timer =
Number.isFinite(timeout) && timeout >= 0
? setTimeout(() => {
timedOut = true;
controller.abort(new TimeoutError());
}, timeout)
: null;
let onAttemptAbort;
try {
const operationPromise = Promise.resolve().then(() =>
operation(controller.signal, attempt),
);
const cancellationPromise = new Promise((_, reject) => {
onAttemptAbort = () => reject(controller.signal.reason);
controller.signal.addEventListener("abort", onAttemptAbort, {
once: true,
});
});
return await Promise.race([operationPromise, cancellationPromise]);
} catch (error) {
if (timedOut) throw new TimeoutError();
throw error;
} finally {
if (timer) clearTimeout(timer);
if (onAttemptAbort)
controller.signal.removeEventListener("abort", onAttemptAbort);
outerSignal?.removeEventListener("abort", onOuterAbort);
}
}
// 关键机制:每轮独立 AbortController 合并外部取消与本轮计时器;退避和清理也需响应取消。
async function execute(operation, options = {}) {
if (typeof operation !== "function")
throw new TypeError("operation must be a function");
const {
retries = 0,
timeout = Infinity,
baseDelay = 0,
signal,
shouldRetry = () => true,
} = options;
if (!Number.isInteger(retries) || retries < 0)
throw new RangeError("retries must be a non-negative integer");
for (let attempt = 0; ; attempt += 1) {
try {
return await runAttempt(operation, attempt, timeout, signal);
} catch (error) {
if (signal?.aborted) throw abortReason(signal);
if (attempt >= retries || !shouldRetry(error, attempt)) throw error;
await abortableDelay(baseDelay * 2 ** attempt, signal);
}
}
}
// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
let calls = 0;
console.log(await execute(
() => {
calls += 1;
if (calls < 3) throw new Error("retry");
return 9;
},
{ retries: 2 },
)); // 预期:9
console.log(calls); // 预期:3
console.log(await Promise.resolve(execute(() => new Promise(() => {}), { timeout: 5 })).then(() => false, () => true)); // 预期:true(拒绝)
const controller = new AbortController();
const pending = execute((signal) => abortableDelay(100, signal), {
retries: 2,
signal: controller.signal,
});
controller.abort(new AbortExecutionError("stop"));
console.log(await Promise.resolve(pending).then(() => false, () => true)); // 预期:true(拒绝)
})().catch(console.error);
class TimeoutError extends Error {
constructor(message = "operation timed out") {
super(message);
this.name = "TimeoutError";
}
}
class AbortExecutionError extends Error {
constructor(message = "operation aborted") {
super(message);
this.name = "AbortError";
}
}
function abortReason(signal) {
return signal && signal.reason instanceof Error
? signal.reason
: new AbortExecutionError();
}
function abortableDelay(ms, signal) {
if (signal?.aborted) return Promise.reject(abortReason(signal));
return new Promise((resolve, reject) => {
const timer = setTimeout(done, ms);
function done() {
cleanup();
resolve();
}
function onAbort() {
cleanup();
reject(abortReason(signal));
}
function cleanup() {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
}
signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function runAttempt(operation, attempt, timeout, outerSignal) {
if (outerSignal?.aborted) throw abortReason(outerSignal);
const controller = new AbortController();
let timedOut = false;
const onOuterAbort = () => controller.abort(abortReason(outerSignal));
outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
const timer =
Number.isFinite(timeout) && timeout >= 0
? setTimeout(() => {
timedOut = true;
controller.abort(new TimeoutError());
}, timeout)
: null;
let onAttemptAbort;
try {
const operationPromise = Promise.resolve().then(() =>
operation(controller.signal, attempt),
);
const cancellationPromise = new Promise((_, reject) => {
onAttemptAbort = () => reject(controller.signal.reason);
controller.signal.addEventListener("abort", onAttemptAbort, {
once: true,
});
});
return await Promise.race([operationPromise, cancellationPromise]);
} catch (error) {
if (timedOut) throw new TimeoutError();
throw error;
} finally {
if (timer) clearTimeout(timer);
if (onAttemptAbort)
controller.signal.removeEventListener("abort", onAttemptAbort);
outerSignal?.removeEventListener("abort", onOuterAbort);
}
}
// 关键机制:每轮独立 AbortController 合并外部取消与本轮计时器;退避和清理也需响应取消。
async function execute(operation, options = {}) {
if (typeof operation !== "function")
throw new TypeError("operation must be a function");
const {
retries = 0,
timeout = Infinity,
baseDelay = 0,
signal,
shouldRetry = () => true,
} = options;
if (!Number.isInteger(retries) || retries < 0)
throw new RangeError("retries must be a non-negative integer");
for (let attempt = 0; ; attempt += 1) {
try {
return await runAttempt(operation, attempt, timeout, signal);
} catch (error) {
if (signal?.aborted) throw abortReason(signal);
if (attempt >= retries || !shouldRetry(error, attempt)) throw error;
await abortableDelay(baseDelay * 2 ** attempt, signal);
}
}
}
// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
let calls = 0;
console.log(await execute(
() => {
calls += 1;
if (calls < 3) throw new Error("retry");
return 9;
},
{ retries: 2 },
)); // 预期:9
console.log(calls); // 预期:3
console.log(await Promise.resolve(execute(() => new Promise(() => {}), { timeout: 5 })).then(() => false, () => true)); // 预期:true(拒绝)
const controller = new AbortController();
const pending = execute((signal) => abortableDelay(100, signal), {
retries: 2,
signal: controller.signal,
});
controller.abort(new AbortExecutionError("stop"));
console.log(await Promise.resolve(pending).then(() => false, () => true)); // 预期:true(拒绝)
})().catch(console.error);
复杂度分析
时间复杂度:O(r),不计等待和操作本身。最多 r 次尝试,每轮只做固定次数的状态与计时器处理。
空间复杂度:O(1)。当前尝试只持有固定数量的控制器和计时器;底层操作内存不计。