[控场AI]
· 7 分钟阅读· 3,849 字

Workflow SDK 5.3.1 原子化 start 实现幂等启动

Workflow SDK 5.3.1 原子化 start 实现幂等启动

Workflow SDK 5.3.1 让 start() 原子化认领 hook,以 token 为幂等键消除重复启动竞态。

Workflow SDK 5.3.1 对 start() 函数做出关键更新:可在一次原子操作中同时准入运行并认领 hook。此前“先 getHookByToken() 再 start()”的模式在查询与启动之间存在时间间隙,可能导致重复启动的竞态条件;新方案中,若 token 未被占用则运行准入并立即拥有该 hook,若已被占用则抛出 HookConflictError 并携带 conflictingRunId。开发者可在 API 路由中捕获该错误,用 resumeHook 把重复请求的负载路由到已有运行,返回相同 runId,实现幂等。token 即幂等键,应取自订单 ID、发票 ID 等业务唯一标识;运行内 createHook 使用相同 token 会绑定已认领的 hook,不新增步骤且 getConflict() 返回 null。设置 experimental_minRetention 还可在运行结束后保持 token 不可用,拒绝迟到的重复请求。

Workflow SDK 的 start() 函数近期迎来一项关键更新:现在可以在一个原子步骤中同时接纳一次运行(run)并认领一个 hook(钩子)。这意味着重复的工作流启动会安全地失败,而不是引发竞态问题。开发者只需升级至 workflow 5.3.1 并参考官方幂等性文档即可使用。

Workflow SDK 幂等启动更新示意

从 getHookByToken 到原子 start:消除竞态窗口

此前,若想实现“先检查 hook 是否存在,再决定是否启动工作流”,通常采用 getHookByToken() 然后调用 start() 的模式。这一模式存在一个文档中明确指出的缺陷:在查询与启动之间存在时间间隙,可能导致重复启动的竞态条件。新的原子化 start() 将“准入运行”和“认领 hook”合并为不可分割的操作,从根源上消除了这个窗口。

原子 start 的核心逻辑是:调用 start() 时传入 experimental_hook: { token },若 token 尚未被任何运行占有,则运行被准入且该 hook 立即归其所有;若 token 已被占用,start() 抛出 HookConflictError,并携带占用者的 conflictingRunId,重复的启动请求永远不会变成新的运行。

竞态条件(Race Condition) 指多个请求或线程并发访问共享状态时,最终结果依赖于它们执行的先后时序,从而可能产生非预期的结果。在本文场景中,两个几乎同时到达的重复请求可能都通过 getHookByToken() 查询到 hook 尚未被占用,随后都调用 start(),于是同一个业务动作被启动两次。这类问题难以复现,却可能在流量高峰或重试机制下真实发生。原子操作(Atomic Operation) 则指不可被中断、要么全部完成要么完全不生效的操作序列,其他并发请求无法观察到其中间状态。把“准入运行”和“认领 hook”合并为一次原子操作后,两个并发请求中只有一个能成功占用 token,另一个必然失败,竞态窗口被从结构上消除,而不再依赖开发者的检查顺序。

代码模式:重复请求路由到已有运行

官方示例展示了在 API 路由中如何使用这一特性。以下代码位于 app/api/orders/route.ts:

import { resumeHook, start } from "workflow/api";
import { HookConflictError } from "workflow/errors";
import { processOrder } from "./workflows/process-order";

export async function POST(request: Request) {
  const { orderId, note } = await request.json();
  const token = `order:${orderId}`;

  try {
    const run = await start(processOrder, [orderId, note], {
      experimental_hook: { token },
    });
    return Response.json({ runId: run.runId, started: true });
  } catch (error) {
    if (!HookConflictError.is(error)) throw error;
    // 将重复请求的负载路由到已拥有该 token 的运行
    const hook = await resumeHook(token, { note });
    return Response.json({ runId: hook.runId, started: false });
  }
}

这段代码清晰地体现了幂等启动的意图:首次请求成功启动运行;重复请求捕获 HookConflictError 后,通过 resumeHook 把携带的数据发送给已有运行,并返回相同的 runId(started 标记为 false)。整个过程无需额外的分布式锁或数据库唯一约束。

幂等性(Idempotency) 是指同一操作执行一次与执行多次对系统状态产生的影响相同。在 HTTP API 与分布式系统中,客户端超时重试、网络抖动、用户重复点击都会导致同一请求被多次投递,若接口不具备幂等性,就可能重复下单、重复扣款。常见的实现手段包括数据库唯一约束、分布式锁、幂等键表等,但都需要额外的存储与协调逻辑。本文中的方案把业务唯一标识(如 order:${orderId})作为 token,由 SDK 内部保证“恰好一个运行拥有该 token”,重复请求会被明确拒绝并路由到已有运行,相当于把幂等键机制内置到了工作流启动环节,开发者无需再自行维护去重表或加锁。

运行内订阅:createHook 绑定已认领的 Hook

被认领的 hook 从运行准入那一刻起就存在,因此 resumeHook 甚至在运行开始执行之前就能工作,早期负载会被缓冲并按顺序投递。在 workflow 内部,使用相同 token 调用 createHook({ token }) 会绑定到已认领的 hook——这是有保证的,不会注册新 hook、不增加额外步骤,且 getConflict() 返回 null。

示例中的 workflows/process-order.ts 展示了订阅模式:

import { createHook } from "workflow";

type OrderUpdate = { note: string };

export async function processOrder(orderId: string, note: string) {
  "use workflow";

  await recordNote(orderId, note);

  // 绑定到 start 时认领的 hook:不会冲突,不产生额外步骤
  const updates = createHook<OrderUpdate>({
    token: `order:${orderId}`,
  });
  for await (const update of updates) {
    await recordNote(orderId, update.note);
  }
}

该模式让工作流可以按顺序消费负载,而无需担心 hook 尚未创建或已被占用。

Hook(钩子) 在此处指工作流中用于接收外部事件或数据的挂起点:工作流可以暂停等待某个 hook 被触发,外部系统则通过 token 向该 hook 投递负载。传统模式下 hook 由工作流内部 createHook 创建,外部必须先知道它已存在才能投递,因此存在“投递早于创建”的时序问题。原子 start 改变了这一模型:hook 在运行准入的瞬间就被创建并归属于该运行,外部投递的数据会被缓冲并按到达顺序排队,等工作流执行到订阅点时依次消费。这种“先有 hook、后有运行体”的顺序,使早期负载不会丢失,也让 createHook({ token }) 成为单纯的绑定操作而非新建操作。

Token 即幂等键:来自业务域的唯一标识

官方强调,token 就是幂等键,并且应当来自你自己的业务域——例如订单 ID、发票 ID、导入 ID 或请求 ID。仅凭 token 就能保证幂等性:恰好一个运行拥有它,重复的 start() 抛出 HookConflictError 并携带占用者的 conflictingRunId,绝不会变成新运行。工作流甚至不必打开这个 hook;如果它从未打开,token 只作为幂等键使用,运行会正常完成。

这一设计将幂等性的责任从外部存储转移到了 Workflow SDK 内部,减少了开发者需要编写的样板代码。对于订单处理、支付回调、数据导入等常见场景,开发者可以直接使用业务唯一标识作为 token,省去额外的去重逻辑。

保留策略与升级建议

设置 experimental_minRetention 可以在运行结束后保持 token 不可用(从 start() 开始计时),因此迟到的重复请求仍会被拒绝。这对于需要在一定时间窗口内防止重复提交的场景非常有用。

要使用这些特性,请升级至 workflow 5.3.1 并阅读官方幂等性文档。原子 start 的引入标志着 Workflow SDK 在简化分布式工作流幂等性方面迈出了务实的一步:把竞态问题交给 SDK,把业务逻辑留给开发者。

#Workflow SDK#幂等启动#原子 start#HookConflictError#createHook#resumeHook#工作流幂等性
分享:

相关推荐