退款申请可能等待财务两天,过程中 Worker 发布了新版本。用一个一直不退出的 Python 函数等待两天,进程稍有故障就失去位置。Temporal 将这种长期流程的执行进展交给服务端持久保存,再由 Worker 根据历史恢复。
理解它的关键不是“函数永远不会失败”,而是把确定性的编排与可能失败的外部工作分开。
谁运行代码,谁保存历史
Client 提交 Workflow,Temporal Service 保存执行历史并调度任务,Worker 轮询任务队列并运行应用代码。Worker 可以更换,服务端保留的历史仍然存在。
Workflow Task 推进编排逻辑,例如决定安排一个 Activity、创建定时器或等待消息。Activity Task 执行外部工作,例如读数据库、调用模型或提交支付。
两种 Task 的失败含义不同。Workflow Task 失败可能是代码与历史不兼容;Activity 失败可能是支付接口暂时不可用。不能把所有异常都理解成“再调用一次模型”。
为什么重放不会重复每一次支付
假设历史记录了“Activity 已完成,结果为回执 R17”。重建 Workflow 时,代码再次走到等待该 Activity 的位置,运行时根据历史提供已记录的结果,而不是因为普通控制流经过这一行就再次执行支付。
但是如果 Activity 调用了支付,Worker 在向服务端报告完成前崩溃,服务端可能需要重试 Activity。这仍是外部成功、本地执行记录未知的边界。因此 Activity 依然需要稳定业务操作 ID。
Temporal Workflow 文档中的确定性要求,约束的是相同历史下的编排行为。它不是所有外部系统都自动恰好一次执行的保证。
哪些代码可以放在 Workflow
根据已有状态判断是否需要审批、等待信号、安排 Activity、等待持久定时器,适合放在 Workflow。直接调用 HTTP、读取会变化的文件、随意读取系统时间,不适合放在确定性编排中。
如果 Workflow 第一次根据当前时间选择退款,重放时当前时间已变,可能生成与原历史不同的命令。应使用框架提供的确定性时间等接口,或将需要记录的非确定性结果放到 Activity 中。
模型输出也具有非确定性且依赖外部服务,通常作为 Activity 结果进入历史。不要在重放时重新问模型“上次你应该决定什么”。
一个等待审批的 Python Workflow
完整代码在 temporal_workflow.py下载,启动与 Worker 在 temporal_demo.py下载。它们不调用模型,Activity 复用本系列的模拟支付账本。
# 等待的是工作流状态条件,Worker 重启后可依历史重新构造这个状态。
await workflow.wait_condition(lambda: self.decision is not None)
# 数据库和支付逻辑在 Activity 内,重试策略必须考虑它可能产生副作用。
self.status = await workflow.execute_activity(
"apply_refund",
args=[task_id, self.decision],
start_to_close_timeout=timedelta(seconds=30),
schedule_to_close_timeout=timedelta(minutes=3),
)
该片段略去类定义与重试配置,以完整文件为准。命令分为 worker、start、approve/reject 和 status,需要已经运行的 Temporal Server。环境准备与示例验证范围见实践指南。
Signal、Query 与 Update 各回答什么
Signal 提交异步消息,例如审批决定;Query 读取当前状态,不修改流程;Update 用于需要由 Workflow 处理并返回结果的交互,可以有验证逻辑。官方消息文档进一步说明三者差异。
示例使用 Signal 接收决定,Query 查询状态。信号发送成功不代表退款已经完成,客户端仍需观察业务结果。示例仅接受第一个有效决定,真实审批服务应记录决定 ID、认证身份和冲突原因。
同样,收到批准不是永久授权。Activity 真正执行前仍应检查当前订单和审批有效范围。Workflow 历史保存的是过去发生的事实,不冻结外部权限。
超时必须知道在计哪一段
Schedule-to-start 关注 Activity 在队列里等待多久;Start-to-close 限制一次执行尝试;Schedule-to-close 覆盖排队、执行和重试的总体时间。Heartbeat timeout 用于检测应持续汇报进度的长 Activity。
例如下载文件一次允许三十秒,但包括退避和重试最多三分钟。只设置一次执行超时,却允许无限重试,用户可能等不到明确结果。
Heartbeat 可以携带进度,帮助后续尝试继续工作;它不自动保存任意 Python 局部变量,也不能让支付服务撤销已发生的动作。取消也需要 Activity 合作处理,并核对外部状态。
沿源码看一次激活
本篇固定 Python SDK 提交 ab25ed6,对应仓库版本 1.32.0。worker/_workflow_instance.py中的 activate 接收桥接层提供的激活信息,设置当前时间、重放标志与历史大小,再处理不同类型的工作。
继续看 _apply 的分派,会遇到定时器触发、Activity 结果、Signal、Query 等分支。它们帮助读者把“历史推动流程”从一句抽象描述还原成具体输入。
不要仅凭这一个 Python 文件推断全部服务端调度。Python SDK 通过桥接层与底层运行时交互,持久历史和队列还涉及 Temporal Service。源码阅读需要保留这些边界。
升级代码为什么仍需谨慎
旧历史先安排 A 再安排 B,新代码改成先 B 再 A,重放时可能不兼容。应使用框架支持的版本策略、兼容分支或 Worker 部署机制,而不是把所有长期任务无条件切到新代码。
长期循环还会积累历史。Continue-As-New 用新的执行承接必要状态,避免历史无限增长,但它需要明确带过去哪些业务身份、未完成工作和去重信息。不能把已退款的操作身份丢掉。
Temporal 适合需要长期等待、跨服务执行和明确恢复机制的流程。它把大量执行基础设施交给成熟系统,同时仍要求业务代码正确处理幂等、授权和版本兼容。选择它,是接受一套明确的编排模型与部署成本。
与 Agent 图组合时谁负责恢复
可以让 Temporal 管理长时间等待、跨服务步骤和补偿,把短时 LangGraph 调查图放进 Activity。此时 Activity 重试可能重新进入 Agent 图,需要稳定关联业务 task ID 与图 thread_id。若两层都保存状态,还要明确哪个系统决定业务终态,以及旧图如何重入。
例如 Temporal 已记录“资料调查完成”,便可复用该 Activity 结果;若调查图完成但 Activity 完成回报丢失,下一次尝试应加载原图或业务产物,避免无界重复调查。不要为每个小函数同时引入两套持久机制,只有恢复粒度确实需要时才组合。