参考
CommandService
command.Service:Execute 语义、中间件顺序、过滤器、校验、命令队列、AfterCommit 钩子,以及 Obs 如何挂接。
CommandService
包:command
command.Service 是写操作的唯一入口。一次 Execute 就是一个数据库事务加一个 UnitOfWork。
结构
type Service struct {
Middleware []Middleware
Filters []Filter
Router *Router
Repo Committer // 通常是 *aggregate.Repository
Tx eventstore.TxManager
Obs *obs.Instruments // 可选
}
err := svc.Execute(ctx, cmd1, cmd2) // 一个事务,一个 UoW
Router 与 Tx 必填,任一为 nil 时 Execute 返回错误。没有命令时直接返回 nil。
通常用 sequent.New 拿到配好的 service,见 接线。
Execute 流程
- 把命令复制进队列。
- 在 context 上创建 AfterCommit 钩子容器。
- 设置了
Obs时,开始 span 与计时。 Tx.WithinTx开启事务。- 创建 UnitOfWork 并挂到 context。
- 队列非空时,取出下一条命令,执行下面的步骤。
- 队列清空后,事务提交。
- 记录遥测数据。
- 没有错误时,执行 AfterCommit 钩子。
每条命令:
EnsureMeta
→ 中间件链(外层先行)
→ Filters(AuthZ)
→ Validate(命令实现了才执行)
→ Router.Dispatch → Handler
→ Repo.Commit
任何错误都会中止整次调用:事务回滚,什么都不落库,AfterCommit 钩子也不会跑。
中间件
type Middleware func(ctx context.Context, cmd Command, next HandlerFunc) error
Chain 外层先行:下标 0 最先执行。典型顺序:
tenancy.Middleware:把 context 中的租户抄到Meta.TenantID。- Idempotency 中间件:检查
(TenantID, IdempotencyKey)(同一业务键重复提交只生效一次)。
tenancy 要放在 Idempotency 前面,因为幂等键空间依赖租户。bank 演示就是这样做的。sequent.WithIdempotency 会追加到 Service.Middleware,所以需要自己把 tenancy 放到最前面:
app.Service.Middleware = append(
[]command.Middleware{tenancy.Middleware},
app.Service.Middleware...,
)
中间件可以不调用 next 直接返回来短路。Idempotency 中间件对重放的键就是这样做的。
过滤器
type Filter interface {
Filter(ctx context.Context, cmd Command) error
}
过滤器在中间件链内部、校验之前运行。sequent.WithAuthorizer(auth) 会安装 security.Filter{Auth: auth},拒绝时返回 security.ErrDenied。见 AuthZ。
校验与分发
命令实现了 Validator 时执行 Validate()。随后 Router.Dispatch 按 CommandType() 查找 Handler,找不到就是错误。
提交
分发之后,Repo.Commit(ctx, RecordOf(cmd)) 追加事件并发布。如果错误是 ErrOptimisticConcurrency 且设置了 Obs,service 会先调用 RecordConflict。见 Repository。
命令队列
Workflow 可以在事务内再追加一条命令:
command.Enqueue(ctx, followUp)
入队的命令排在队列已有命令之后,在同一事务与 UnitOfWork 中执行。见 Workflow。
AfterCommit 钩子
用 workflow.RegisterAfterCommit(ctx, fn) 注册的钩子只在事务提交后执行。钩子的错误会从 Execute 返回,但已提交的事件保持不变。不应回滚账本的工作适合放在钩子里。
批量执行
svc.Execute(ctx, cmd1, cmd2, cmd3)
所有命令共用一个事务。批量调用的 OpenTelemetry 标签是第一条命令的类型加 +batch。任何一条失败,全部不落库。
可观测性
Obs 是一个字段,不是中间件。用 sequent.WithObs(inst) 设置,或直接赋值 svc.Obs = inst。见 OpenTelemetry。
错误处理
err := svc.Execute(ctx, cmd)
switch {
case errors.Is(err, eventstore.ErrOptimisticConcurrency):
// 带退避重试,重新 Load
case errors.Is(err, eventstore.ErrUniqueKeyViolation):
// 业务重复
case errors.Is(err, security.ErrDenied):
// 授权失败
case idempotency.IsReplay(err):
// Strict 模式:键已处理过
}
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。