夜雨聆风学习资料网

ARTICLE · 1278058

我开源了 Temporal4L 从源码和故障实验理解持久化执行

我开源了 Temporal4L 从源码和故障实验理解持久化执行

订单已经扣款,正在等待人工确认。这时,把执行代码的 Worker 停掉,再发送确认消息,会发生什么?

重启之后,流程还能继续吗?已经扣过的钱会不会再扣一次?如果停机期间等待超时了,谁来处理后续清理?

这些问题,也是理解 Temporal 持久化执行时绕不开的问题。为此,我最近开源了Temporal4L · Temporal Lab:一个围绕订单流程展开的中文 Temporal 学习实验台。你可以在浏览器里创建订单、注入故障、观察真实执行历史,再沿着源码解释每一步为什么发生。

  • 项目地址:github.com/likebeans/temporal4l
  • 在线学习:likebeans.github.io/temporal4l

在线工作台可以直接阅读课程、查看源码和进行教学推演;想亲手停止 Worker、验证恢复或查看支付账本,可以启动本地实验室。项目中的 Workflow 真实运行在 Temporal 上,库存、支付、打包和发货采用模拟业务,不会产生真实扣款。具体功能与入口见项目说明。

从一条订单理解 Temporal

Temporal4L 选择了一条容易理解、又足以暴露可靠性问题的业务流程:

预留库存 → 收取货款 → 等待人工确认 → 可选分批打包 → 创建发货单。

付款可能失败,确认可能迟到,打包可能中断,用户也可能中途取消。把这些情况加进去之后,几个核心问题就自然出现了:哪些步骤应该重试?已经产生的业务效果怎么处理?执行进程消失后,流程依据什么恢复?

项目使用 TypeScript 和 Temporal SDK 实现编排,使用 React 提供中文实验界面,Node API 负责连接浏览器与 Temporal。模拟支付另有一份 SQLite 账本,用于核对实际入账效果。技术栈与依赖

一次 await 背后的调度过程

先看 orderWorkflow 的主干。下面按实际实现压缩展示,省略了状态展示、消息 handler、可选打包与补偿分支:

await reserveInventory(activityArgs);inventoryReserved = true;paymentAttempted = true;const payment = await chargePayment(activityArgs);paymentCharged = true;const approvedInTime = await condition(  () => approvalReceived,  approvalTimeoutMs,);if (!approvedInTime) {  throw ApplicationFailure.nonRetryable(    '人工确认超时',    'ApprovalTimedOut',  );}const trackingNumber = await shipOrder(activityArgs);

这段代码表达的是步骤之间的依赖:库存成功后付款,付款成功后等待确认,确认后发货。理解它的关键在于:这些业务函数来自 proxyActivities,实际的 Activity 函数体在 Worker 的 Activity 执行器中运行。

在本项目中,API 通过 client.workflow.start 创建执行,传入 Workflow 类型、输入、Workflow ID 和 Task Queue。启动成功后就返回执行标识,没有调用 handle.result() 等订单结束。Server 保存启动事件并安排 Workflow Task;Worker 主动轮询任务队列,领取任务后运行编排。API 与执行流程

两类任务承担不同工作

任务
执行什么
产生什么
Workflow Task
根据已有事件推进编排,处理消息和等待条件
调度 Activity、启动 Timer、结束 Workflow 等命令
Activity Task
运行库存、支付、打包等具体业务函数
成功结果、失败或心跳

本项目由同一个 Worker 进程处理这两类任务,它们在执行语义上各自独立。以第一次库存预留为例,主要交互如下;图中两个执行器表示 Worker 内的不同职责。

sequenceDiagram    participant API as API 与 Client    participant S as Temporal Server    participant W as Workflow 执行器    participant A as Activity 执行器    API->>S: 启动订单 Workflow    S->>S: 保存启动事件并安排任务    W->>S: 轮询 Workflow Task    S-->>W: 返回任务和执行历史    W->>S: 提交本轮结果和调度库存命令    S->>S: 记录 ActivityTaskScheduled    A->>S: 轮询 Activity Task    S-->>A: 返回库存任务与参数    A->>A: 执行 reserveInventory    A->>S: 报告 Activity 成功结果    S->>S: 保存结果并安排后续编排任务    W->>S: 轮询后续 Workflow Task    S-->>W: 返回 Activity 完成事件    W->>W: 推进 await 并继续付款步骤

具体而言,调用 Activity 代理会生成调度命令,Worker 在完成本轮 Workflow Task 时提交它。Server 据此记录 ActivityTaskScheduled 并安排 Activity Task。Activity 执行完成后,Worker 上报结果,Server 将结果记入 History;后续 Workflow Task 再让 SDK 推进对应 Promise,代码才执行 inventoryReserved = true。

这里的Command 是编排提出的操作请求,Event 是 Server 已经记录的执行事实。例如,安排付款是一个命令,付款被调度和付款完成则是不同事件。这个区分解释了为什么调用代理、业务执行和历史落盘发生在不同阶段。

等待不会占住一个专属线程

await 表示当前逻辑需要等待结果。等待 Activity、外部确认或 Timer 时,整个长流程可以由多轮短的 Workflow Task 推进,无需保持最初的 HTTP 连接,也无需给每个等待中的订单分配一个持续阻塞的线程。

这也不意味着每条 await 都严格对应一次任务提交:已有结果可以直接推进,一轮编排也可能产生多个命令。需要关注的是 SDK 如何在事件到达时继续执行,而不是给每行代码机械地分配一个任务。项目调度过程导读

WorkflowTaskCompleted 只表示一轮编排处理完成。订单的最终完成需要看到 WorkflowExecutionCompleted。两个名字相近,含义却处于不同层次。

History 如何让 Worker 重启后恢复

假设库存和付款都已完成,订单正在等待人工确认。这时停止 Worker,进程里的 state、inventoryReserved、paymentCharged 等普通内存变量会随进程退出消失。

恢复的依据保存在 Temporal Server 的执行历史中。对于这条订单,History 包含启动输入、已经安排的 Activity、已记录的成功结果、创建的 Timer,以及停机期间接收到的 Signal 等事件。

History 保存的是执行事件与相关数据,局部变量的值通过重放代码重新建立。例如,inventoryReserved = true 这次普通赋值,并不要求 Server 为它单独存一份变量快照。项目恢复机制

已经完成的付款如何被重放

新 Worker 领取任务后,SDK 可以从 orderWorkflow(input) 入口重放。当代码再次走到对应 Activity 调用时,SDK 将产生的命令与既有 History 匹配,并利用已记录的完成结果推进等待。

可以把这条订单的恢复过程拆开看:

重放到的代码位置
既有历史提供的依据
恢复后的行为
await reserveInventory(...)
库存 Activity 的调度与成功结果
复用历史结果,继续执行后面的赋值
inventoryReserved = true
相同代码路径再次执行
重新建立库存已预留这个局部事实
await chargePayment(...)
付款 Activity 的成功返回值
将历史结果交给等待中的代码
paymentCharged = true
相同代码路径再次执行
重新建立已确认付款的状态
等待确认的 condition
Timer 与收到的确认事件
恢复等待,或继续处理已收到的确认

因此,编排代码可以重新经过付款那一行,而已经记录成功的付款 Activity 无需仅因重放再执行一次。复用的是与该次调用匹配的历史结果;它也不是按函数名或参数做一个全局缓存。之后真正新调度的付款 Activity,仍是新的执行。

运行中的 Worker 还可以缓存 Workflow 状态,后续任务不必每次都从入口完整重放。进程重启或缓存丢失时,才需要利用相应历史重新建立状态。缓存减少恢复工作,持久化历史提供恢复依据。Temporal 执行与缓存

确定性为什么必不可少

重放成立的前提,是相同历史能够驱动出与既有事件兼容的命令序列。

假设第一次执行时先安排库存、再安排付款;恢复时却因为读取了一个变化的外部配置,改成先发货。此时新生成的命令与旧历史无法对应,SDK 就不能可靠地解释这个 Workflow 应处于什么状态。

因此,网络请求、数据库查询、文件读取等会随外部世界变化的操作,应放在 Activity 中,让返回结果进入执行历史。Workflow 根据这些已记录结果做分支和编排。

确定性也不等于禁用所有时间 API。本项目的 Workflow 使用了 Date.now() 和 new Date();在 Temporal TypeScript 的 Workflow 运行环境中,它们具有 SDK 提供的确定性语义。Activity 中的相同代码则运行在普通 Node 环境。修改正在运行的 Workflow 时,同样需要考虑旧历史兼容性,按变更选择 Patch 或 Worker Versioning 等机制。项目确定性说明·官方确定性约束

在实验中核对恢复

跑通正常订单后,可以进入“恢复实验”。先按向导单独启动 API、Worker 和前端,保证对应任务队列只有一个 Worker,再完成三个阶段:

  1. 保存基线。 等待库存和付款完成,记录 Workflow ID、Run ID、历史事件与支付账本。
  2. 停机收件。 在终端停止 Worker,并在向导中标记阶段。先建立 Query 不可用时的 History 边界,再通过官方 Temporal UI 发送无参数的 approveOrder Signal;保持 Worker 停止,核对新 Signal 已进入边界之后的历史。
  3. 重启核对。 重启 Worker,并在向导中开始核对同一个 Run 的后续事件:编排继续、订单完成,库存与付款未重新调度,付款调用数及扣款数保持不变。

普通确认 API 会先执行 Query,Worker 停机时可能卡在这个预检,所以这组实验使用官方 Temporal UI 发送 Signal。完整准备与判定条件见恢复向导。

人工确认的等待如何跨越进程退出

订单付款后会执行:

const approvedInTime = await condition(  () => approvalReceived,  approvalTimeoutMs,);

当确认条件尚未成立、又设置了超时时间时,SDK 会通过 Temporal 的持久化 Timer 表达这个等待。等待期限从付款成功、进入确认阶段开始计算,前端显示的倒计时只是辅助观察。

确认 Signal 或 Update 被 Worker 处理后,会改变 approvalReceived。条件成立,等待结束,流程继续安排打包或发货;如果 Timer 到期而条件仍未成立,condition 返回 false。本项目随后显式抛出 ApprovalTimedOut,由异常分支执行补偿。

这形成了三个独立的动作:Server 管理期限,Worker 执行条件与分支判断,业务代码定义超时后的退款和库存释放。Worker 停机期间,Server 仍可记录 Timer 到期;具体补偿需要等 Worker 可用后执行。因此停止 Worker 不会暂停订单的确认期限。

如果确认与超时非常接近,应依据 History 和 Workflow 的实际处理结果解释最终分支。浏览器按钮的点击时间不足以确定执行顺序。持久化等待的项目实现

重试和超时如何协作

理解重试之前,需要先问清楚:这次重试作用在哪一层?

本项目没有配置 Workflow 整体重试。Worker 重启恢复原 Run,Activity 重试在这个 Run 中产生新的尝试;显式配置的 Workflow 整体重试则属于另一层机制,会开始新的 Run。

在 Temporal4L 的自动重试场景里,前两次 chargePayment 故意返回可重试错误,第三次返回成功。这三次是同一个付款 Activity 的不同尝试,订单仍在同一个 Workflow Run 中,已经完成的库存步骤不会因为付款重试而重新执行。

付款策略在 workflows.ts 中定义:

retry: {  maximumAttempts: 3,  initialInterval: '1 second',  backoffCoefficient: 2,  maximumInterval: '3 seconds',}

Server 根据这个策略安排新的 Activity Task。第一次失败后的退避约为 1 秒,第二次失败后的退避约为 2 秒;实际观察到的耗时还包含业务执行与调度时间。maximumAttempts: 3 包含首次执行,因此本例最多只会发生两次重试,3 秒的退避上限在这组配置下不会被用到。

Workflow 中的 await chargePayment(...) 等待最终成功或最终失败。中间仍可重试的错误由 Activity 重试机制处理;达到尝试上限、遇到不可重试错误等终结条件后,失败才会传回 Workflow 的异常处理分支。项目重试说明·Temporal 重试策略

三种期限分别约束什么

以项目中的长任务 packOrder 为例:

配置
本项目的值
约束范围
startToCloseTimeout
45 秒
某一次 Activity 尝试从开始到结束的最长时间
scheduleToCloseTimeout
2 分钟
这个 Activity 从最初被调度到最终结束的总预算,包含排队、退避和各次尝试
heartbeatTimeout
5 秒
Activity 开始后及运行期间允许缺少心跳的时间窗口

例如,任务总共可能处理很久,心跳期限能让 Server 在单次运行预算耗尽之前发现长时间没有报告的情况;单次执行期限限制某次尝试的最长耗时,总预算则防止整个 Activity 因排队与多次尝试一直拖延。心跳本身只能说明 Activity 在报告状态,实际业务进度还要查看检查点内容。

超时表达的是 Server 对某次执行的判断。网络中断时,Server 可能收不到心跳,但旧 Worker 的代码仍在运行。Server 无法凭一个超时判断强制终止远端进程或撤销已发出的外部请求。这也是重试必须配合幂等的原因。Activity 故障检测

还有一个观察细节:Temporal 会聚合 Activity 的中间重试记录,不能要求 History 展示每次尝试各一组完整的 Started、Failed。查看最终成功的 attempt、运行期间的 Pending Activities、Worker 日志和支付账本,才能更完整地解释发生了什么。重试的运行证据

支付幂等处理的是两个系统之间的故障窗口

前面说过,已记录成功结果的 Activity 可以在重放时复用。那么,为什么付款还可能重复调用?关键在于支付账本和 Temporal History 分别保存自己的状态,它们之间没有共享的原子事务。

一次付款至少涉及两个事实:

  1. 支付侧已经提交扣款。
  2. Temporal 已经记录付款 Activity 的成功结果。

这两个事实之间存在时间窗口。如果 Worker 在账本提交后、结果写入 History 前退出,Temporal 无法仅凭已有历史确认扣款成功。为了继续推进,它可能根据超时和重试策略重新执行 Activity;支付侧则需要识别这是同一次业务操作。

在提交扣款后故意报告失败

Temporal4L 的“支付幂等”场景直接暴露这个窗口。故障位于 payments.charge 返回之后,核心代码如下:

const { reused } = payments.charge(workflowId, total, attempt);if (input.scenario === 'idempotency' && !reused) {  payments.recordFailure(    workflowId,    attempt,    '扣款已入账,但本次 Activity 在报告成功前故意失败',  );  throw ApplicationFailure.retryable(    '模拟扣款成功后响应丢失',    'PaymentResponseLost',  );}

第一次执行已经完成账本事务,但随后抛出可重试错误,Temporal 会安排下一次尝试。第二次调用进入同一份账本,查询到这条付款已经存在,便复用已有结果。

正常完成这组实验时,账本显示Activity 调用 2 次,模拟扣款 1 笔。History 中的最终成功结果说明 Temporal 最终收到了哪一次尝试的成功,账本则说明产生了几次业务效果。付款 Activity 源码

稳定标识和原子入账缺一不可

这个示例的扣款键为 ${workflowId}:charge。PaymentStore.charge 在一个 BEGIN IMMEDIATE 事务中完成查询、校验和入账:已有相同 Workflow ID 的付款时,先核对金额再返回复用结果;没有记录时,插入付款记录与扣款流水。表中的 workflow_id 是主键,idempotency_key 还有唯一约束。支付账本源码

稳定键负责识别“是不是同一次业务操作”,事务与约束负责保护“判断和入账”这个整体。重试时若生成新键,去重就失去依据;查询与写入若缺少原子性保护,并发请求也可能都认为自己应该执行扣款。

这组实验体现的是:Activity 可能被再次调用,业务侧让这些重复调用收敛到一次可接受的记账效果。这里的保证覆盖本地模拟账本;接入真实支付时,需要使用支付服务支持的幂等能力、状态查询与对账。

幂等范围还必须与业务身份一致。本项目每次新建订单都会生成新的 Workflow ID,所以重新提交一份表单会创建新订单,不会自动复用原付款。真实业务中,应根据订单号等业务标识设计 Workflow ID 与支付幂等键的对应关系。创建订单的身份规则

补偿如何处理成功了一半的流程

当付款已完成、人工确认却超时,系统需要执行退款与释放库存。退款是一项新的业务操作,需要单独调度、执行并确认结果;原有扣款记录仍然存在。

Temporal4L 把这些动作写在 compensate 中,并为每一步记录 pending、running、completed 或 failed。退款和释放库存分别捕获错误,因此某项补偿失败不会阻止下一项补偿被尝试。

为什么付款前就记录 paymentAttempted

主干中的顺序很有意义:

paymentAttempted = true;const payment = await chargePayment(activityArgs);paymentCharged = true;

paymentCharged 只在 Workflow 收到成功结果后才变成 true。如果扣款已经发生、成功结果却没有返回,单看这个变量会漏掉需要退款的情况。

因此,补偿采用 paymentCharged || paymentAttempted 判断是否需要查账:只要付款已经被尝试,就调用幂等的 refundPayment。账本没有扣款时不产生退款;已有扣款且尚未退款时完成退款;重复调用不会重复生成退款效果。这个判断直接承接了上一节的故障窗口。

为什么清理需要 nonCancellable

取消请求具有协作语义。Workflow 收到取消时,原取消作用域已经被标记为取消;如果继续在这个作用域里安排退款,清理动作本身也可能立刻受到取消影响。

所以项目使用:

await CancellationScope.nonCancellable(compensate);

它让补偿可以在外层取消后继续执行,但补偿仍可能遇到业务错误、Activity 超时或重试耗尽。代码需要记录这些结果,实际业务也需要为未完成的补偿设计后续处理。

这解释了界面上一个看似矛盾的状态:业务可能已经显示 cancelled,Temporal 仍是 RUNNING,因为退款和库存释放尚在进行。最终执行状态、补偿明细与账本需要一起核对。强制终止 Workflow 则不会正常走这套取消清理分支;项目使用的是 cancel。取消与补偿实现

心跳如何支持长 Activity 的恢复与取消

Workflow 的历史重放可以恢复编排,但一个运行中的 Activity 还可能有自己的内部进度。例如,12 批打包已经做完 7 批,Worker 在第 8 批期间退出了。

重新尝试 Activity 时,新的函数调用会从入口开始执行。要继续剩余批次,业务代码需要读取检查点并主动跳过已确认完成的部分。

检查点如何进入下一次尝试

Temporal4L 的 packOrder 通过 context.heartbeat(checkpoint) 报告进度。检查点包含 completedBatches、totalBatches、attempt、resumedFrom 等字段。新尝试启动后,从 context.info.heartbeatDetails 读取上次保存的信息,校验批次数,再从 resumedFrom + 1 开始循环。打包 Activity 实现

这里有三个连续的动作:业务代码构造检查点,SDK 将心跳发送给 Server,Server 保存的详情在下一次尝试中交还给 Activity。自动重试提供重新执行的机会,恢复到哪个批次由 Activity 自己解释检查点。

“心跳超时恢复”场景会在首次尝试的第 3 批附近停止发送心跳。超过 5 秒心跳期限后,Server 根据策略安排重试;新的尝试读取上次实际送达 Server 的进度,再继续处理。

检查点和业务效果之间也有窗口

调用一次 heartbeat() 不代表这条进度已经在 Server 端保存。SDK 会对心跳进行节流,传输也可能延迟。

假设本地已经做完第 7 批,Server 最后保存的检查点仍是第 6 批。新尝试就需要从第 7 批继续;如果第 7 批已经向外部系统写过数据,这些写入可能被再次尝试。

因此,实际批处理可以为每一批使用稳定的业务标识,让“重复处理某一批”得到可接受的结果。保存检查点也要符合业务完成顺序:先把尚未完成的批次标记为完成,会导致恢复时跳过必要工作。本项目的打包只模拟处理时间与进度,这个窗口由实验帮助读者理解。

取消请求为什么也需要心跳

对本项目使用的普通 Activity,心跳还是接收取消通知的渠道。Server 将取消请求传递给 Activity 上下文,业务代码再通过可取消的 context.sleep() 等操作响应。

Workflow 配置了 WAIT_CANCELLATION_COMPLETED,会继续等待 Activity 在 Temporal 中形成终结结果,例如成功、失败、取消或相应超时。服务端结束这次等待仍不等于远端函数已经物理停止,外部副作用依然需要业务代码控制。

项目由此把两种恢复展示得很清楚:Workflow 依靠 History 重放重建编排;Activity 依靠重试和业务检查点恢复内部进度。两者的恢复粒度、执行位置与幂等责任各有范围。心跳与取消实验·官方 Activity 取消说明

Signal Update 和 Query 分别确认了什么

人工确认环节还有一层容易混淆的语义:消息已经发出去、Workflow 已接受确认、订单已经发货,分别由什么结果证明?

Temporal4L 同时提供 approveOrder Signal、approveOrderUpdate Update,以及只读的 orderState Query。

Signal 确认 Server 收件

Client 调用 Signal 后,Server 可以接收并持久化这条消息,再安排 Workflow Task。Worker 处理相应历史事件,调用 Signal handler,由 handler 修改状态或推动流程。

这解释了为什么 Worker 停机时仍可以先发送 Signal:Server 保存消息,Worker 恢复后再处理。但 Signal 的成功回执不等待 handler,也不返回它的业务结果。本项目的 applyApproval 只在允许确认的阶段改变状态,提前或重复的确认会被忽略。

所以,看到 WorkflowExecutionSignaled 能证明消息进入了历史;是否应用确认,还需要看 approvedAt、approvalMethod 等业务状态,发货则需要继续核对后续 Activity。

Update 把校验与处理结果交给调用方

Update 在 Workflow 内执行 validator 和 handler。本项目的 validator 检查是否勾选核对信息、订单是否正在等待确认;handler 再检查当前条件,然后调用 applyApproval 修改状态,并返回确认结果。

阶段
代表什么
接受前被 validator 拒绝
这次业务请求未通过校验,不能按已接受 Update 解释
Accepted
Update 已被接受,尚不能据此判断 handler 最终成功
Completed
handler 已形成 outcome,可能是成功,也可能是失败

项目中成功的 handler 结果是 status: approved,表示人工确认已应用。后续打包和发货仍由主流程继续执行。validator 拒绝通常没有对应的 Accepted、Completed 事件,应查看本次真实回执;Completed 也要结合 outcome 判断成功与否。

这些是逻辑上的阶段,不要求每个阶段都单独占用一个 Workflow Task。本例的 handler 是同步的,校验、接受与完成可以在同一轮任务处理中推进。项目的 Signal 确认 API 有前置 Query,但查询与后续发送之间,业务状态可能变化,因此它不能替代 Workflow 内门禁。Update API 则直接提交 Update,由 Workflow 内的 validator 和 handler 校验。项目消息对照·TypeScript 消息机制

Query 读取 Worker 中的业务状态

orderState 的 handler 返回当前 state,不修改 Workflow。Query 不增加对应的持久化业务消息事件,需要由 Worker 执行只读 handler 回答;缓存不可用时,Worker 可能先通过历史重建状态。

因此,Server 能返回执行状态,不意味着 Query 必然成功。Worker 不可用时,Describe 与 History 仍可能可读,Query 却暂时无法回答。这正是项目区分业务状态来源与执行状态来源的原因。Query 路径说明

请求超时后为何保留原 Update ID

项目 API 返回 unknown 时,表示这次 HTTP 等待没有取得确定结果。Update 可能仍在处理,也可能已经成功而响应没有及时返回;unknown 本身不表示 Workflow 已取消或确认未生效。

恢复时,需要保留原 Workflow ID、Run ID、Update ID 与参数,查询原 Update 的结果,或者重试同一请求。换一个新 Update ID,会把原请求的重试变成另一个请求。

同一个 Run 内,已接受 Update 的身份和结果复用帮助客户端处理不确定响应,其范围还受到服务端保留期限约束。这个机制与支付侧的业务幂等各自负责一层:前者识别 Workflow 消息,后者处理外部操作可能重复执行的效果。validator 的拒绝也不能当作永久保存的去重结果。未知结果恢复

把三种超时放在一起,分工会更清楚:

到期的是哪一个
说明什么
接下来怎样处理
HTTP 等待期限
当前请求没有及时得到结果
核对原请求的身份和结果
人工确认 Timer
业务允许等待确认的期限已到
Worker 执行超时分支,按代码补偿
Activity 超时
某次尝试或整个 Activity 超出预算
Server 按超时类型和策略重试,或报告最终失败

它们约束不同的等待,不能把前端请求结束理解成后台业务已经结束。

用真实证据验证执行机制

flowchart TD    UI[”浏览器实验台”] -->|HTTP 请求| API[”Node API 与 Temporal Client”]    API -->|启动与消息交互| TS[”Temporal Server”]    WK[”Worker 执行 Workflow 与 Activity”] -->|轮询任务与提交结果| TS    TS --- H[”持久化执行历史 History”]    WK -->|模拟扣款与退款| DB[”SQLite 支付账本”]    API -->|读取支付记录| DB

这几个角色各司其职:Workflow 定义业务顺序与分支,Activity 执行具体操作,Worker 运行代码,Temporal Server 保存执行历史并调度任务。页面创建订单后,API 返回执行标识,后续步骤由 Worker 异步推进。对应入口分别是workflows.ts、activities.ts、worker.ts和index.ts。

实验台把流程进度和运行证据放在一起。读者既可以看订单走到了哪里,也可以追问:这个状态从哪里来?

想知道什么
项目读取的来源
订单是否正在等待确认,补偿进行到哪一步
Workflow Query
执行是否仍为 RUNNING,当前未完成 Activity 的尝试号与最近心跳是什么
Temporal Describe
哪些 Activity 已完成,Signal 何时到达,Timer 是否触发
Temporal History
支付调用了几次,真正模拟扣款和退款各几笔
独立 SQLite 支付账本

这些来源分别回答不同问题。例如,收到取消后,业务状态可以先变成 cancelled,但退款与释放库存仍在执行,Temporal 的执行状态暂时保持 RUNNING。理解这种差异,才能判断流程是否真的结束。源码与运行证据导读

在“源码与证据”标签中,还可以沿着当前问题查看真实源文件、符号和行号,再对照这个 Run 的状态与事件。看到付款重试后,继续找到故障注入发生的位置;看到补偿进行中,再读到对应的清理分支。这样,实验现象就能和代码中的决定一一对应。源码与证据入口说明

这些读取也不是一个跨系统原子快照。项目先后读取 Describe、Query、History 和账本,各项数据可能反映不同进度;Query 失败后保留的旧业务状态不能代表当前状态。因此阅读时要结合页面的新鲜度提示,用同一个 Run 的事件顺序和后续观察验证结论。观测来源与新鲜度

七个场景与一条循序渐进的学习路径

围绕同一条订单,项目目前提供七个实验场景:

场景
重点观察
正常处理
库存、付款、确认与发货如何协作
自动重试
付款前两次失败,第 3 次成功,观察 attempt 与退避
失败与补偿
付款耗尽尝试次数后,执行退款检查与库存释放
等待超时
人工确认等待 15 秒后超时,进入失败与补偿
支付幂等
扣款入账后报告失败,下一次调用复用结果
长任务心跳
观察 12 批打包进度,以及运行中的协作取消
心跳超时恢复
停止心跳后触发重试,从已保存检查点继续

对刚接触 Temporal 的读者,我建议沿着项目的五阶段路径学习:先跑通正常订单,理解角色分工;接着做 Worker 恢复;再学习重试、超时与幂等;随后观察取消、补偿与心跳;最后深入比较 Signal 和 Update。每个阶段都有操作目标、预期证据和源码入口。五阶段学习路径

在线工作台提供七章课程、章节搜索、教学推演、九个主题的真实源码、自测和 Markdown 笔记导出。它适合先建立概念,再回到本地实验验证自己的预测。在线推演明确标注为教学示意,GitHub Pages 只托管静态内容,真实运行需要本地 Temporal、API 和 Worker。在线学习说明

完成订单实验之后,还可以做一组独立的“资料审核”迁移练习:自行实现准备资料、等待确认、归档或清理,再验证恢复、幂等、超时与取消。项目提供题目、参考实现和 CLI,并使用独立任务队列与数据库,让你尝试把学到的机制放进另一条业务流程。资料审核练习

在本地跑起来

先准备Node.js 22.13 或更高版本和 npm。选择 Docker 启动方式时,还需要启动 Docker Desktop 或 Docker Engine。

git clone https://github.com/likebeans/temporal4l.gitcd temporal4lnpm cinpm run temporal:upnpm run dev

等待 Temporal 就绪、应用启动后,可以打开:

  • 中文实验台
  • Temporal 官方 UI,选择 temporal-learning 命名空间

npm run dev 会同时启动前端、API 和 Worker。第一次可以创建一条正常订单,等它进入人工确认,再发送 Signal,观察发货与最终完成。

如果希望不用 Docker,完成克隆和 npm ci 后,在两个终端分别运行:

# 终端 A:首次运行需要联网下载官方开发服务器npm run temporal:local# 终端 B:等待终端 A 显示 Temporal 已就绪npm run dev

两种 Temporal 启动方式选择一种即可。做 Worker 停机实验时,按恢复向导将 API、Worker 和前端拆成独立进程。端口、数据保存位置与更多启动说明见README。

本地环境采用开发服务器配置,适合学习与实验;实际业务接入仍需要另外设计认证、部署运维、Workflow 代码演进与外部操作幂等。

欢迎一起完善这个学习实验台

如果你正在学习 Temporal,或者正在思考人工审批、批处理、异步任务中的恢复与重试问题,欢迎从一条正常订单开始,再亲手让它经历一次失败。

我希望 Temporal4L 能帮助读者形成一种具体的学习习惯:操作前写下预测,操作后核对历史、状态和账本,最后回到代码解释结果。项目的自测与实验笔记也围绕这个过程设计。

项目代码和文档采用MIT 协议。欢迎通过Discussions交流学习问题,通过Issues提交可复现的问题,也欢迎按照贡献指南补充教学说明、实验和测试。

如果这个项目对你有帮助,欢迎给Temporal4L一个 Star,也欢迎把它分享给正在学习 Temporal 的朋友。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料