跳到主要内容

Dex vs. 其他引擎

查看 Markdown

Dex 和其他 durable execution 工具的目标相同:让流程在崩溃、重启和长时间等待之后继续运行。本页把每类工具的术语对应到 Dex,说明设计上的不同之处,以及何时其他工具更合适。

从 Temporal 过来? 按这个顺序读:

  1. 本页的主要区别和确定性要求转移到了哪里。
  2. Step,了解保证和默认值。
  3. Step 的进阶保证,了解重新投递、结果未知,以及 SYNC 和 ASYNC Step。
  4. 故障转移和部署,了解哪些会重新运行。
  5. 修改 Flow 代码,了解不依赖重放的版本管理。
  6. Attribute 锁,了解并行 Step 共享的状态。
  7. Client,了解怎样启动 Flow 和发送命令。
  8. 性能和限制。

共同点

本页的每种工具都在流程之外记录流程的进度。崩溃之后,流程从这份记录继续。这些引擎、运行时和库还都提供持久化 timer、等待外部事件和重试。Agent 框架的 checkpointer 覆盖的范围更小;它的小节说明它把哪些事留给你。

主要区别:显式的 Step 图取代重放代码

大多数 durable execution 工具让你把流程写成一个函数,再通过重放这个函数重建它的状态。Dex 则记录 Step 的结果。一个 Flow 是一组 Step。每个 Step 返回一个决定,指明接下来运行哪些 Step,或者完成 Flow,Dex 把结果和这个决定一起记录下来。

  • 得到什么。 Step 代码就是普通代码。它可以读取时钟、使用随机数、调用任何库。修改代码永远不会让正在运行的 Flow 发生重放或非确定性错误。状态保存在 Attribute 里,而不是事件历史里。
  • 代价是什么。 你要把控制流建模成 Step 和决定。循环是一个转到自己的 Step,分支是一个有两个目标的决定。

确定性要求转移到了哪里

一条规则取代了重放规则。Step 可能运行不止一次,所以每个外部效果只能依赖 Dex 传给这个 Step 的内容;见幂等键。本页的每种工具都把同样的责任留给你,故障证明也以此为前提。时钟和随机值可以用在 Step 的决定和保存的数据里,但不要用在幂等键里,也不要用在重新运行时必须完全一致的效果里。

并发的默认行为

Dex 在不同的 Worker 上并行运行同一个 Flow 的多个 Step,RPC 也可以和它们同时运行。它们共享 Flow 的 Attribute,由你选择哪些 Attribute 需要锁。Temporal workflow 在单线程上运行,Restate virtual object 每个 key 只允许一个独占 handler,所以两者默认都只有一个写入方。

同一个流程的两种写法

一个提醒循环每隔一段时间发送一次提醒,直到对方选择退出。在 Temporal 里,循环是由引擎重放的普通代码。下面这段代码只是示意,不来自 Dex 示例:

Python
@workflow.defn
class ReminderWorkflow:
def __init__(self) -> None:
self.opted_out = False

@workflow.signal
def opt_out(self) -> None:
self.opted_out = True

@workflow.run
async def run(self) -> None:
while True:
try:
await workflow.wait_condition(
lambda: self.opted_out, timeout=REMINDER_INTERVAL
)
return
except asyncio.TimeoutError:
await workflow.execute_activity(
send_reminder, start_to_close_timeout=timedelta(seconds=30)
)

在 Dex 里,循环是一个转到自己的 Step。它等待 Timer 或退出 Channel,再由它的决定选择接下来做什么。下面的摘录来自可运行的 提醒 模式:

reminder_flow.pyPython
class ReminderStep(Step[None]):
def __init__(
self,
service: ServiceDependency,
opt_out: Channel[None],
) -> None:
self.service = service
self.opt_out = opt_out

def wait_for(self, context: Context, input: None) -> Wait:
return Wait.any_of(
Timer.by_duration(REMINDER_INTERVAL),
self.opt_out.for_one(),
)

def execute(self, context: Context, input: None) -> StepDecision:
if not context.has_timer_fired():
return graceful_complete()
self.service.send_email("Reminder: please respond", "Hello, ...")
return go_to(ReminderStep, None)

例子: examples/python/dex_examples/patterns/reminders/reminder_flow.py

提醒不带幂等键,因为多发一次提醒没有坏处。在 Rust 里,has_any_timer_fired 询问这个 Wait 中是否有任意一个 Timer 已触发。Temporal 版本从上往下读,靠重放重建。Dex 版本给每个状态命名,由 Dex 保存等待和决定。

如果你了解 iWF:Dex 由它发展而来。Dex 的 Step 就是 iWF 的 State,WaitFor 就是 WaitUntil。

基于重放的 workflow 引擎

Temporal 和 Cadence 是最知名的例子。Worker 根据记录的事件历史重放 workflow 函数,以重建它的状态。副作用放在 activity 里,workflow 代码只能通过确定性的 API 读取时间或随机数。

Temporal 或 Cadence 术语最接近的 Dex 术语
Workflow type、Workflow ID、Run IDFlow type、Flow ID、Run ID
ActivityStep Execute 里的工作,它是一个重试单元。每个持久化调用都放进自己的 Step。SYNC Step 要等两次对象存储提交;延迟见性能。
activity 之间的 workflow 代码Step 的决定:GoTo、GoToMany、GracefulComplete、DeadEnd
SignalChannel 消息。FlowService PublishToChannel 在没有 Worker 在线时也能保存它;见发布消息。
Query只读 RPC,或者从 Client 直接读取 Attribute
Update写入 Attribute 或发布到 Channel 的 RPC。它需要在 10 秒内有 Worker 接手,并且没有按 update ID 去重。
Timer 或 sleepStep WaitFor 里的 Timer 条件
Child workflowSubFlow
Task queue 和 sticky queueFlow type 队列,带尽力而为的 sticky 路由。要把工作交给单独的 Worker 池,就给它单独的 Flow type。
Search attributes建了索引的 Attribute
ResetTime Travel,保留同一个 Run ID
Continue-As-New用不到。Dex 为每个 run 保留一个最新历史事件的窗口,并清理更早的事件;见 History window(英文)。
Local activity最接近的是 ASYNC Step,它在结果保存之前就继续往下走,如果 server 节点先出故障,它会重新运行。
Cancellation scopeRPC 按 Step 类型取消正在运行的 Step,StopFlow 停止整个 Flow。取消是协作式的;见取消正在运行的 Step。
Schedule一个在 Timer 上循环的 Flow;见 Cron 定时调度。

相同之处。 每个 Flow ID 只有一个活跃的 run,并有 ID 复用策略。两者都使用 pull 模式的 Worker;持久化 timer 和等待都不占用 Worker;每个工作单元有自己的重试策略。

区别。

  • Worker 读取保存的状态,而不是重放历史,所以它用不到历史缓存。见 Routing and availability(英文)。
  • 版本管理靠保留旧的 Step 类型并做增量修改,不需要 patch 标记。修改 Flow 代码说明规则,部署前检查给出手动兼容性检查。
  • Dex 的 Step 在不同的 Worker 上并行运行。Temporal 的单线程仍然会在 await 点交错运行 handler,所以差距比“一个写入方”听起来要小,但差距确实存在。
  • Step 方法的重试复用第一次尝试的状态快照。activity 的重试会读取最新数据。
  • 重试的 RPC 或重复的 Channel 发布会再次运行;Temporal 会丢弃重复的 update,并尽力丢弃重复的 signal。见限制。
  • Client 根据在自己语言里注册的 Flow 定义来启动 Flow。其他语言的服务通过 gRPC 调用 FlowService StartFlow;见从 API 服务启动 Flow。
  • server 故障转移会重新运行每个进行中的 Step 尝试。见故障转移和部署。
  • Dex Server 是一个运行在对象存储上的二进制文件,有一个 active 节点和若干 standby。Temporal 由多个服务组成,运行在数据库上。

基于日志的 durable function 运行时

Restate 是一个例子。handler 通过运行时 context 执行每个副作用,运行时把结果记录在日志(journal)里。失败之后,handler 再次运行,已记录的结果直接返回。带 key 的 virtual object 保存状态,每个 key 只有一个写入方。

Restate 术语最接近的 Dex 术语
WorkflowFlow
Service handler一个没有 Attribute 的短 Flow。Service handler 是一次持久化、记录了日志的调用,有重试和幂等键,但没有 key-value 状态。
记录日志的操作(ctx.run)Step Execute
Virtual object 及其 key每个 key 一个长期运行的 Flow,用 key 作为 Flow ID。已关闭的 run 在保留期后被删除,而 virtual object 会一直存在。
Object stateAttribute
Awakeable 或 durable promiseStep 等待的 Channel 消息
Durable sleepTimer
Shared handler不写入 Attribute 的 RPC

相同之处。 崩溃后复用已记录的副作用结果。状态按你选择的 ID 分 key 保存。

区别。

  • 日志调用之间的 handler 代码必须是确定性的,因为它会再次运行。Dex 每次都重新运行 Step 代码,所以修改代码不会破坏正在运行的 Flow。
  • virtual object 每个 key 只允许一个写入方。同一个 Flow 的 Step 和 RPC 可以同时运行,Attribute 锁按需启用。
  • Restate 按幂等键对调用去重。Dex 按 Flow ID 和 request ID 对 Flow 启动去重,目前重复的 Channel 发布和 RPC 会再次运行。
  • Restate 的 UI 和 CLI 显示一次调用的日志。Dex 把每个 Step 的输入、输出和错误随 run 一起保存到保留期结束,Dex Web 在 Flow 的图和 Timeline 上按 Step 显示它们。

基于数据库的 workflow 库

DBOS 是一个例子。workflow 和 step 是你应用里加了注解的函数,checkpoint 保存在你的 PostgreSQL 数据库里。崩溃之后,workflow 再次运行,已完成的 step 返回记录下来的输出。

DBOS 术语最接近的 Dex 术语
Workflow、Workflow IDFlow、Flow ID
StepStep Execute
Send 和 receiveChannel
Events(set 和 get)Attribute,通过 RPC 或 Client 读取
Durable sleepTimer
QueueFlow type 队列。它没有按队列设置的并发或速率限制;每个 Worker 的任务槽位限制它的工作量。

相同之处。 普通的应用代码,加上记录下来的 step。Workflow ID 让启动具有幂等性。

区别。

  • DBOS 的 workflow 函数在 step 之间必须是确定性的。
  • DBOS 的 send 接受幂等键。目前 Dex 重复的 Channel 发布可能投递两次。
  • DBOS 只用你自己运行的 PostgreSQL 数据库,不增加其他 server。Dex 在对象存储上运行自己的 server,并且可以把 Attribute 值同步到你的数据库。

Step function 服务

AWS Step Functions 是一个例子:用 JSON 定义语言写的状态机,每个状态调用一个服务。两者都记录一张显式的状态图。Dex 的 Step 和决定是用你的语言写的代码,运行在你的 Worker 上,所以 Step 可以调用任何库。

Agent 框架 checkpoint

LangGraph 是一个例子。checkpointer 在每一步之后按 thread ID 保存图的状态,interrupt 让图暂停,等待人来处理。它的托管平台负责恢复 run。

Agent 框架术语最接近的 Dex 术语
Thread IDSession ID,也就是 Flow ID
Checkpoint每次模型调用或 Tool Call 之后 Session 的 Attribute
等待人工输入的 interrupt在 AwaitToolApproval 中等待的 Tool Call,或者一个提问
Node 或 Tool Call一个 Step

checkpointer 负责保存状态;重启崩溃的 run,或者用 timer 唤醒它,需要托管平台或你自己的代码。Dex Server 为每个 Session 做这些事。使用 Dex Agents 时,你继续用自己的框架,由适配器把 Agent 包装进一个 Session Flow;见 持久化 Agent。

运行在 durable execution 引擎上的 Agent 框架

运行在 Temporal 上的 OpenAI Agents SDK、运行在 Temporal、DBOS 或 Prefect 上的 Pydantic AI,以及 LangGraph 平台,都把每次模型调用和包装过的 Tool Call 记录为引擎的一个单元,所以崩溃只会重复正在运行的那次调用。

  • 在 Dex 中,每个 tool 都有一个 effect class。读取可以再次运行。非幂等写操作默认只尝试一次,如果被中断,会为模型记录 outcome_unknown;见 Tool Call。
  • effect class 也决定默认的审批方式。审批等待不占用 Worker,可以持续几分钟或几天;见 审批。
  • 一个适配器模型和一个 Session Flow 覆盖所有受支持的框架。
  • Dex Server 的版本是 0.1,可以运行在 Dex Cloud 上,也可以运行在你自己的基础设施上。上面这些引擎有更长的生产使用记录。

何时其他工具更合适

  • 你想把流程写成一个从头到尾顺序执行的函数。 基于重放的引擎、基于日志的运行时或基于数据库的库都支持这种写法。Dex 要求你把流程建模成 Step 和决定。
  • 你现在就需要 1.0 版本的 server。 Dex Server 的版本是 0.1。其他几种工具有更长的生产使用记录。
  • 你的流程是一个服务里的一个短函数,而你已经在运行 PostgreSQL。 基于数据库的库不会增加新的 server。
  • 你的团队已经在大规模运行另一个引擎。 迁移的代价会超过本页这些差异带来的好处,除非重放或版本管理错误对你来说确实是个问题。
  • 你希望每个 key 只有一个写入方,而且不用声明锁。 virtual object 或单线程的 workflow 默认就是这样。在 Dex 里,你要声明 Attribute 锁。
  • 你需要一个集群提供超过一个节点的容量。 一个集群由一个 active Dex Server 节点服务。超出这个容量时,你要运行多个独立的集群。见超出一个节点的容量。
  • 你需要 OSI 认可的开源许可证。 Dex 以 Sustainable Use License 提供源代码,这个许可证限制把 Dex 作为平台托管或转售。Temporal 和 DBOS 使用 MIT 许可证。见许可证。

下一步