跳到正文

参考

Workflow

Workflow 与 AfterCommit 钩子:在事务内响应事件、只在提交后做副作用,以及不参与读模型重建。

Workflow

包:workflow

Workflow 响应事件来编排后续工作:可以追加命令、记录状态,并注册只在事务提交之后才执行的副作用。

接口

Workflow 就是一个 publish.Handler:

type Workflow interface {
    publish.Handler // Handles() []string;Handle(ctx, event.Event) error
}

通过门面注册:

app.RegisterWorkflow(&NotifyWorkflow{})

Workflow 在 SyncInTxn(在写事务里同步更新读模型)上运行,晚于同一事件的 Projector(把事件写成查询用读模型的组件)。它们不参与读模型重建:replay 流水线使用 PublishProjectorsOnly,所以重建不会重复发通知,也不会重复发命令。见 Publisher。

AfterCommit

无法回滚的工作——邮件、webhook、调用支付服务商——不能放在事务里跑。如果它已经执行而事务随后回滚,你就把一件没发生的事告诉了外部世界。

请改为注册 AfterCommit 钩子:

workflow.RegisterAfterCommit(ctx, func(ctx context.Context) error {
    // 副作用
    return nil
})

钩子按每次 Execute 收集在 context 上,按注册顺序执行,且只在事务成功提交之后。Execute 因任何原因失败时,一个钩子都不会跑。

钩子失败时,错误会从 Execute 返回,但已提交的事件保持提交状态。钩子失败不会撤销账本写入。请把钩子当作尽力而为;需要重试时用 Outbox。

Bank 示例中的写法

type NotifyWorkflow struct {
    mu       sync.Mutex
    Opened   []string
    Notified []string
}

func (w *NotifyWorkflow) Handles() []string { return []string{EventAccountOpened} }

func (w *NotifyWorkflow) Handle(ctx context.Context, e event.Event) error {
    ev := e.(*AccountOpened)
    w.mu.Lock()
    w.Opened = append(w.Opened, ev.Owner)
    owner := ev.Owner
    w.mu.Unlock()

    workflow.RegisterAfterCommit(ctx, func(ctx context.Context) error {
        w.mu.Lock()
        w.Notified = append(w.Notified, owner)
        w.mu.Unlock()
        return nil
    })
    return nil
}

AccountOpened 发布时,Workflow 立刻记下 owner,并把通知安排到提交之后。测试可以据此断言:只有命令成功时 Notified 才会有内容。

追加命令

Workflow 可以在同一事务里发起更多工作:

command.Enqueue(ctx, &SomeFollowUp{ /* ... */ })

该命令排在队列已有命令之后,在同一个 UnitOfWork 与事务中运行。请把 Meta.Source 设为 workflow 并填好因果字段,这样审计记录能说明命令为何运行。

Workflow 还是 Outbox

场景 选择
提交后的快速本地副作用,丢失可接受 AfterCommit 钩子
必须投递、可重试、可观测 Outbox,由轮询器消费
必须与账本原子完成 同一事务里的 Projector 或后续命令

如果 Workflow 必须调用 Circle API 这类外部服务,请放在 AfterCommit 钩子或 Outbox 投递里,让外部延迟和重试远离账本事务。

规则

  • Workflow 在写事务内运行。只有想回滚账本写入时才返回错误。
  • 重建时不会重放 Workflow,所以不要在里面维护读模型;用 Projector。
  • 尽量让 Workflow 处理器幂等(重复执行也不会产生额外副作用)。命令因冲突重试时它们可能再次运行。

相关

文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。