跳到主内容
创建时间:最后更新:作者:白川

处理流程失败、超时与重试

只重试明确可恢复的只读步骤,传递取消信号,保留失败身份,避免重复执行副作用。

处理流程失败、超时与重试

一个步骤失败后,是停止整个流程、保留部分结果,还是重新执行,需要由程序决定。把整个工作流放进一个无限重试循环,会让已经完成的动作反复发生。

本篇只给可重试的只读步骤增加有限重试。使用 Node 自带的计时器,不需要模型服务。

先区分三种情况

情况合适的处理
参数错误、文件不存在、权限拒绝
修正输入或配置,通常不应盲目重复
已明确识别的短暂故障
在次数和时间预算内重试
用户取消或整体超时
终止等待,不启动下一次尝试

“错误消息里包含 timeout”不是可靠分类器。本例用明确的错误类型演示重试决策,实际接入 HTTP 或 SDK 时需要根据其状态和契约分类。

限制尝试次数并传递信号

创建 src/retry.ts:

typescript
import { setTimeout as delay } from "node:timers/promises"

export class TransientStepError extends Error {}

export async function retryRead<T>(
  execute: (signal: AbortSignal) => Promise<T>,
  signal: AbortSignal,
  maxAttempts = 2,
): Promise<T> {
  if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
    throw new Error("最大尝试次数必须是正整数")
  }
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    signal.throwIfAborted()
    try {
      const value = await execute(signal)
      signal.throwIfAborted()
      return value
    } catch (error) {
      if (signal.aborted || !(error instanceof TransientStepError) || attempt === maxAttempts) {
        throw error
      }
      await delay(100 * attempt, undefined, { signal })
    }
  }
  throw new Error("没有得到结果")
}

只有 TransientStepError 会继续尝试,普通错误立即抛出。等待也接收同一个信号,用户取消时不会睡满时间后再请求一次。

参数叫 maxAttempts,包含第一次执行,不是额外重试次数。与底层 SDK 叠加时,还需要检查 SDK 自身的重试策略,否则表面两次调用可能对应更多网络请求。

运行一份确定的失败样本

创建 src/failure-main.ts:

typescript
import { retryRead, TransientStepError } from "./retry.js"

let calls = 0
const signal = AbortSignal.timeout(2_000)
try {
  const value = await retryRead(async () => {
    calls += 1
    if (calls === 1) {
      throw new TransientStepError("教学模拟:第一次只读请求暂时失败")
    }
    return { source: "fixture", checked: true }
  }, signal)
  console.log({ status: "completed", calls, value })
} catch (error) {
  console.error({ status: signal.aborted ? "cancelled" : "failed", calls, error: String(error) })
  process.exitCode = 1
}

运行:

bash
node --import tsx src/failure-main.ts

预期显示两次尝试后完成。第一次失败是程序主动制造的教学故障,不是声称恢复了真实服务。把异常换成普通 Error,则应第一次就失败。

示例把信号中止统一归入 cancelled,它可能来自时间上限,也可能来自用户操作。产品中如果需要分别展示,应继续记录并判断中止原因,不把所有中止都写成用户主动取消。

保留步骤身份,不从头重做

在更长的流程里,错误至少应关联步骤名称、尝试次数和任务身份。前面已经完成的读取或分析结果可以在本次运行中保留,但内存里的变量不等于退出后仍能恢复。

并行流程可以继续使用带身份的结果数组;顺序流程应停止依赖失败步骤的下游动作。不要为了产出一份“完整报告”而用空数据代替失败输入。

为什么不把这个函数用于任意写入

发送消息、保存文件和调用外部业务接口可能已经生效,只是响应丢失。此时简单重试会产生重复操作。要先确认动作是否允许重做、是否有幂等键,以及怎样核对结果未知的情况。

AbortSignal 也不是事务回滚。已经完成的动作不会因为后续取消而撤销,忽略信号的第三方代码可能继续运行。因此调用者既要发送取消,还要等待受管理的工作结束。

下一篇实现一个明确范围内的任务保存与继续,实际观察进程退出后的恢复和写入去重。

参考资料