8.5 KiB
ADR-0032:Approved Action Dispatch 执行租约与 Start Barrier
- 状态:Proposed
- 日期:2026-07-19
- 关联:QL-RFC-0001、ADR-0031、ADR-0033、ADR-0034、ADR-0005、ADR-0014
上下文
ADR-0031 已保证 Approval 的一次性消费与 immutable ApprovedActionDispatch 同事务提交,但 durable handoff 本身不等于外部副作用已执行。若 lease 过期后无条件让另一个 Dispatcher 再次调用 Tool、Package installer 或资源写入,旧执行可能已经产生副作用,只是结果尚未写回;自动 takeover 会把一次审批变成重复写入。
另一方面,只用内存队列会让控制面重启后丢失 pending 动作,只有一个全局进程锁又无法支持 cluster-control 多副本。执行侧需要与 Run cancellation/dispatch 一致的短事务 claim、owner/token/version fencing,同时必须比普通“至少一次消息”更严格地区分副作用前和副作用后故障。
决策
1. Immutable dispatch 与 mutable execution control 分表
0021-approved-action-dispatch-executions 新增一对一 ApprovedActionDispatchExecutions:
dispatch_id引用 immutableApprovedActionDispatch;- Project、status、version、attempt/max-attempt;
- 单一
eligible_at_ms、可选 retry time; - lease owner/token/expiry;
- execution start、result mutation、低敏 result code 和 completion time;
- 创建/更新时间。
0021 为升级窗口中已经存在的 dispatch 回填 pending@v0。此后 Approval consume 必须在同一事务写 Approval consumed@v3、immutable dispatch 和 execution baseline;任一步失败全部回滚。dispatch 不承担 lease/result 更新,避免审批证据被执行器覆盖。
2. 状态机区分副作用前和副作用后
pending
-> leased
-> retry_wait -> leased
-> blocked
-> executing
-> succeeded
-> failed
-> blocked (indeterminate)
pending/retry_wait到期后可 claim;leased表示尚未跨越副作用 start barrier,lease 过期可以安全 takeover;executing表示 start barrier 已提交,handler 可能已经产生副作用;executinglease 过期只产生有效状态recovery_required,不得回到 due queue;succeeded/failed/blocked为终态;- 只有明确发生在 start barrier 前的失败可以 retry;attempt budget 耗尽后 blocked。
eligible_at_ms 只在 pending/leased/retry_wait 非空,使 edge 和 cluster 都可以用同一有界索引扫描。executing 不进入普通 due scan,防止一个通用 worker 把不确定副作用自动重放。
3. Claim、renew、start 和 result 均使用 fencing
SQLite adapter 使用短 BEGIN IMMEDIATE transaction 和 (dispatch_id,status,version,lease_owner,lease_token) 条件更新:
- claim 增加 attempt/version 并写 lease;
- 同 owner/token 的未过期 claim 精确重放;
- 过期且尚未 start 的 lease 可以由另一个 token takeover;
- start 必须匹配 ApprovalRequest ID、action digest、owner/token/version,并发生在 lease expiry 前;
- renew 只允许当前 leased/executing fence;
- preflight release 和 execution completion 使用 result mutation ID 幂等;
- stale owner、token、version 或 action identity 全部 fail closed。
executing 不允许 takeover,因此原 owner 的可信结果可在 lease 到期后提交,只要它仍匹配没有被替换的 execution fence。未来 recovery resolver 与迟到结果竞争时也必须由同一数据库事务裁决,不能根据 wall clock 覆盖可信终态。
PostgreSQL adapter 必须保持相同 contract,可使用行锁、条件更新和 SKIP LOCKED;不得让多个控制面共享本地 SQLite。
4. Handler 在 start barrier 前只能 inspect
ApprovedActionHandler 分为:
inspect(dispatch):必须 side-effect-free,解析 immutable action plan 并返回实际 canonical action digest;execute(context):只在executing已持久化后调用,接收 dispatch ID idempotency key 和 owner/token/version fence。
inspect 返回 digest 漂移、显式 blocked 或 extensible/非法结果时,不调用 execute。handler 缺失和 inspect 暂时失败可以在 start 前有界重试。execute 一旦被调用:
- 明确 success/failure 记录对应终态;
- handler throw、transport 消失或无法证明结果时记录
indeterminate -> blocked; - completion 持久化失败时保持 executing/recovery-required,不调用 preflight retry,也不再次 execute。
handler 仍必须让下游接受 dispatch ID 作为幂等键或提供可查询 receipt。start barrier 只能阻止 QingLong 自动重放,不能单独提供跨数据库和外部系统的 exactly-once。
5. Bounded Dispatcher 不拥有 timer
当前 ApprovedActionDispatcher.dispatchBatch() 每次只读取一页,页上限 64,固定一次 due observation,并逐条 claim/inspect/start/execute/complete。它没有内部 timer、递归翻页、后台进程或生产注册;调度 cadence、页数上限、shutdown 和 profile 资源预算由后续 lifecycle owner 决定。
edge/standalone 可以用较慢 cadence 和小页;cluster-control 使用 PostgreSQL 多节点 claim。二者共享状态机,但不共享 adapter 或本地文件。
6. 当前切片保持 production unreachable
当前已经具备 execution migration、SQLite repository、handler port、bounded dispatcher,以及 ADR-0033/0022 的 recovery control/resolution repository 和 evidence-only reconciler。ADR-0034/0023 已提供第一个默认不可达的 run.create handler、同事务 Run receipt 和 SQLite evidence provider,但仍没有:
- Tool/Package/Secret 等外部 mutation handler,或
run.create的 production plan resolver; - 通用 canonical action plan/Artifact resolver 和其他下游 idempotency receipt;
- 强认证人工处置 API、production lifecycle 或 PostgreSQL evidence provider;
- lifecycle timer、startup/shutdown、Profile cadence、指标和告警;
/api/v3Approval UI/Event/Audit;- PostgreSQL adapter 与跨方言 contract suite;
- 可信 owner bootstrap issuer 和生产 authentication wiring。
因此 app/loaders/api/services/shared/data 不得导入该 dispatcher/repository。当前 run.create 只能在显式 contract test 中执行,现有 Shell、Scheduler、Tool 和 HTTP 路径均不可达。
影响
正面影响:
- pending 动作可跨重启恢复且支持多 Dispatcher fencing;
- pre-start crash 可安全 takeover,post-start crash 不会自动重复副作用;
- digest drift 在任何 handler 副作用前阻断;
- completion 丢失显式进入 recovery,而不是猜测成功或重新执行;
- edge 只有有界索引扫描和短事务,空闲时零内部 timer。
代价与风险:
- 每个 approved dispatch 增加一行 execution control 和多个短事务;
recovery_required需要后续 receipt/query resolver,否则需要人工处置;- handler 必须满足 inspect 无副作用、actionRef 不可变和下游 idempotency contract;
- 当前没有生产 handler,功能仍不可达。
未选择的方案
- leased 过期一律重新 execute:无法区分副作用是否已发生,拒绝。
- Approval consumed 后直接同步调用 handler:崩溃后无 durable claim/result,拒绝。
- 把 mutable lease/result 写回 immutable dispatch:混淆审批证据和执行控制,拒绝。
- handler exception 自动 retry:异常可能发生在副作用之后,拒绝。
- 用内存队列或进程锁去重:重启丢失且不支持 cluster 多副本,拒绝。
- 一个后台 timer 扫完整表:edge 写放大和停机不可控,拒绝。
验证要求
0021能回填已有 dispatch,schema ownership 覆盖表、索引和 migration;- Approval consume 的三表写入原子,execution insert 失败时 Approval/dispatch 回滚;
- due 查询使用稳定 keyset、固定页上限且不返回 executing/terminal;
- 双 SQLite 连接对一个 due dispatch 只有一个 claim,其余看到 live lease;
- expired leased 可以 takeover,expired executing 只能 recovery-required;
- start 精确绑定 ApprovalRequest/action digest/owner/token/version;
- preflight retry 有 attempt budget,耗尽后 terminal blocked;
- success/failure/indeterminate result 有 fencing 和精确 mutation replay;
- handler execute 只发生在 durable start barrier 后;
- execute 后异常、completion 写失败都不进入自动 retry;
- Node 22/24 全量测试、类型检查、migration chain、schema audit 和 GitNexus reachability 通过;
- 生产目录不得导入或装配本切片。