跳到正文
Elaine Blog
返回

读懂 DBOS:数据库怎样让普通 Python 程序获得持久执行能力

更新于:
Agent Runtime

一个 Python 函数依次创建申请、等待审批、调用支付。普通程序崩溃后,局部变量和执行位置消失。DBOS 将工作流与步骤的执行信息保存到数据库,让程序能够根据记录继续。

它值得加入 Runtime 学习路线,因为其入口接近普通函数,同时源码中可以清楚看到“先检查是否有记录,再决定执行”的机制。本篇固定提交 83805fd,不把未来版本行为混入讲解。

Workflow 与 Step 为什么分开

Workflow 表达执行顺序,Step 封装外部工作。工作流恢复时,运行时需要知道某次步骤是否已有结果。如果把所有外部调用藏在没有记录的普通函数里,就无法可靠区分哪些工作已经完成。

@DBOS.workflow()
def refund(task_id: str, directory: str):
    # create_task 是已注册的 step,业务任务本身也使用稳定身份去重。
    create_task(task_id, directory)
    # recv 由 DBOS 记录等待与收到的消息;不是进程内 input()。
    decision = DBOS.recv(topic="approval", timeout_seconds=3600)
    # 完整示例处理超时、输入校验,再进入保存决定与支付的 step。

完整实现见 dbos_demo.py下载。这段代码不生成模型回答,先让执行机制可见,再考虑把模型调用放进 Step。

步骤怎样找到之前的结果

沿 _core.py的 invoke_step 阅读,会看到当前工作流身份、函数序号和步骤名称参与操作查询。

check_existing_result 调用系统数据库检查操作执行记录。如果有已保存输出,就反序列化并返回;如果记录的是错误,则恢复对应错误;没有记录才进入实际函数执行。

record_step_result 在函数返回或异常后序列化相应结果并写入操作记录。这解释了“为什么重放可以跳过已有步骤”,也直接暴露了边界:函数产生外部副作用之后、记录结果之前仍可能崩溃。

为什么仍然需要支付幂等键

假设 Step 调用支付成功,下一行记录结果前进程崩溃。DBOS 数据库看不到完成结果,恢复后仍可能再次进入函数。数据库无法凭空知道另一个服务已经做了什么。

示例因此复用 RuntimeStore 与 PaymentSimulator。支付以稳定 operation ID 查回执并拒绝参数冲突。DBOS 负责步骤执行记录,业务代码负责远端账本核对。

如果业务写入和某种框架事务机制确实处于同一个受支持的数据库事务中,可以获得更强的局部原子性;但不能把这个结论延伸到任意 HTTP 请求。阅读项目时应分别核对事务 API、数据库后端和实际提交范围。

等待消息为什么也要持久化

审批等待使用 recv,外部命令使用 send。固定快照中 recv 把工作流 ID、操作序号、主题和超时交给系统数据库处理,而不是只等待一个内存队列。

send 的消息去重键与业务 decision ID 各司其职:前者控制重复消息投递,后者识别审批决定。相同去重键不能用来偷偷替换一份已提交决定;需要修正审批时应设计显式业务流程。

示例只接收一次有效审批。等待超过一小时返回 approval_wait_expired,业务任务仍可能保留待审批记录;这是明确标记的教学边界,生产中应让工作流超时与业务状态迁移保持一致,而不是隐藏差异。

数据库、工作流 ID 与应用版本怎样关联

SetWorkflowID 为本次工作流指定稳定身份,重启后应复用相同 ID、数据库与兼容代码。换一个 ID 不是“继续原任务”,而是改变执行身份。

示例使用文件型 SQLite 系统库,与业务任务库、模拟支付库分开。固定快照也包含其他后端实现。选择数据库时要核对支持的并发、队列与事务特性,不能只因本地演示使用 SQLite 就外推到所有部署。

工作流恢复会经过原有调用结构。随意在旧步骤前插入新的操作,可能改变操作序列与历史对应关系。因此代码升级仍需版本策略,普通函数写法没有消除兼容问题。

恢复为什么还涉及队列

_recovery.py并不是找到未完成函数就直接随意调用。该快照先将符合条件的待恢复工作重新入队,再通过队列的状态转换获得执行资格。

恢复查询还涉及 executor ID 和应用版本。这意味着应结合启动配置理解“哪些任务会被当前实例恢复”,不能笼统宣称每个进程启动就接管所有机器上的全部任务。

继续看 _queue.py 与系统数据库接口,能将前文的领取、并发控制和恢复积压联系起来。队列是执行资格与容量管理的一部分,不只是一个 list。

如何观察配套示例

先准备可选依赖,再用 start 命令启动并等待;另一个终端使用 approve 或 reject 发送教学决定。所有命令指向相同数据目录。流程退出后,可以读取业务 SQLite 状态或复用 RuntimeStore 查询。

代码使用 SQLite 本地存储、虚构审批身份和模拟支付。它帮助读者观察操作记录,不替代生产部署中的并发恢复、权限和故障验证。依赖准备及示例验证范围见实践指南。

DBOS 官方工作流教程可帮助理解公开 API;本文固定源码链接用于核对具体实现,二者版本变化时以明确选择的版本为准。

几个项目应该怎样比较

项目本系列重点观察什么仍由应用负责什么
LangGraph图状态、节点调度、Checkpoint、人工中断业务幂等、授权、部署与状态版本
Temporal服务端历史、确定性编排、Activity、长期等待Activity 副作用、业务一致性、代码兼容
AgentScope JavaAgent 调用上下文、会话状态、存储契约交易账本、跨副本并发策略、业务审批
DBOS数据库操作记录、函数式工作流、消息等待与恢复队列外部系统未知结果、业务授权与版本策略

它们并非四个完全等价的替代品。可以用 Agent SDK 实现模型循环,再由持久工作流调度长期任务;也可以在简单应用里只采用一种机制。选择依据应是任务真实的失败边界和等待方式。

回到 A1042,用户关心的最终仍然是:有没有收到申请,谁批准了哪个金额,支付是否发生,崩溃后能否给出一致解释。理解这些问题,再读框架源码,才不会把 API 名称当成可靠性的证明。


分享这篇文章:

上一篇
读懂 AgentScope Java Runtime:会话状态怎样进入业务系统