编码母题 / P0 / 异步编程 / 02

支持超时取消与重试的执行器

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度必练难度困难预计完成50–70 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

包装可能同步或异步的 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);

解题思路

展开思路、不变量与常见边界

关键切入点:每轮独立 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);

复杂度分析

时间复杂度:O(r),不计等待和操作本身。最多 r 次尝试,每轮只做固定次数的状态与计时器处理。

空间复杂度:O(1)。当前尝试只持有固定数量的控制器和计时器;底层操作内存不计。

下载原始 JS 文件