ARTICLE · 1278058
我开源了 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 与执行流程
两类任务承担不同工作
本项目由同一个 Worker 进程处理这两类任务,它们在执行语义上各自独立。以第一次库存预留为例,主要交互如下;图中两个执行器表示 Worker 内的不同职责。
sequenceDiagramparticipant API as API 与 Clientparticipant S as Temporal Serverparticipant W as Workflow 执行器participant A as Activity 执行器API->>S: 启动订单 WorkflowS->>S: 保存启动事件并安排任务W->>S: 轮询 Workflow TaskS-->>W: 返回任务和执行历史W->>S: 提交本轮结果和调度库存命令S->>S: 记录 ActivityTaskScheduledA->>S: 轮询 Activity TaskS-->>A: 返回库存任务与参数A->>A: 执行 reserveInventoryA->>S: 报告 Activity 成功结果S->>S: 保存结果并安排后续编排任务W->>S: 轮询后续 Workflow TaskS-->>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(...) | ||
inventoryReserved = true | ||
await chargePayment(...) | ||
paymentCharged = true | ||
condition |
因此,编排代码可以重新经过付款那一行,而已经记录成功的付款 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,再完成三个阶段:
保存基线。 等待库存和付款完成,记录 Workflow ID、Run ID、历史事件与支付账本。 停机收件。 在终端停止 Worker,并在向导中标记阶段。先建立 Query 不可用时的 History 边界,再通过官方 Temporal UI 发送无参数的 approveOrderSignal;保持 Worker 停止,核对新 Signal 已进入边界之后的历史。重启核对。 重启 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 | ||
scheduleToCloseTimeout | ||
heartbeatTimeout |
例如,任务总共可能处理很久,心跳期限能让 Server 在单次运行预算耗尽之前发现长时间没有报告的情况;单次执行期限限制某次尝试的最长耗时,总预算则防止整个 Activity 因排队与多次尝试一直拖延。心跳本身只能说明 Activity 在报告状态,实际业务进度还要查看检查点内容。
超时表达的是 Server 对某次执行的判断。网络中断时,Server 可能收不到心跳,但旧 Worker 的代码仍在运行。Server 无法凭一个超时判断强制终止远端进程或撤销已发出的外部请求。这也是重试必须配合幂等的原因。Activity 故障检测
还有一个观察细节:Temporal 会聚合 Activity 的中间重试记录,不能要求 History 展示每次尝试各一组完整的 Started、Failed。查看最终成功的 attempt、运行期间的 Pending Activities、Worker 日志和支付账本,才能更完整地解释发生了什么。重试的运行证据
支付幂等处理的是两个系统之间的故障窗口
前面说过,已记录成功结果的 Activity 可以在重放时复用。那么,为什么付款还可能重复调用?关键在于支付账本和 Temporal History 分别保存自己的状态,它们之间没有共享的原子事务。
一次付款至少涉及两个事实:
支付侧已经提交扣款。 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 修改状态,并返回确认结果。
项目中成功的 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 的拒绝也不能当作永久保存的去重结果。未知结果恢复
把三种超时放在一起,分工会更清楚:
它们约束不同的等待,不能把前端请求结束理解成后台业务已经结束。
用真实证据验证执行机制
flowchart TDUI[”浏览器实验台”] -->|HTTP 请求| API[”Node API 与 Temporal Client”]API -->|启动与消息交互| TS[”Temporal Server”]WK[”Worker 执行 Workflow 与 Activity”] -->|轮询任务与提交结果| TSTS --- H[”持久化执行历史 History”]WK -->|模拟扣款与退款| DB[”SQLite 支付账本”]API -->|读取支付记录| DB
这几个角色各司其职:Workflow 定义业务顺序与分支,Activity 执行具体操作,Worker 运行代码,Temporal Server 保存执行历史并调度任务。页面创建订单后,API 返回执行标识,后续步骤由 Worker 异步推进。对应入口分别是workflows.ts、activities.ts、worker.ts和index.ts。
实验台把流程进度和运行证据放在一起。读者既可以看订单走到了哪里,也可以追问:这个状态从哪里来?
这些来源分别回答不同问题。例如,收到取消后,业务状态可以先变成 cancelled,但退款与释放库存仍在执行,Temporal 的执行状态暂时保持 RUNNING。理解这种差异,才能判断流程是否真的结束。源码与运行证据导读
在“源码与证据”标签中,还可以沿着当前问题查看真实源文件、符号和行号,再对照这个 Run 的状态与事件。看到付款重试后,继续找到故障注入发生的位置;看到补偿进行中,再读到对应的清理分支。这样,实验现象就能和代码中的决定一一对应。源码与证据入口说明
这些读取也不是一个跨系统原子快照。项目先后读取 Describe、Query、History 和账本,各项数据可能反映不同进度;Query 失败后保留的旧业务状态不能代表当前状态。因此阅读时要结合页面的新鲜度提示,用同一个 Run 的事件顺序和后续观察验证结论。观测来源与新鲜度
七个场景与一条循序渐进的学习路径
围绕同一条订单,项目目前提供七个实验场景:
对刚接触 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 的朋友。














































































