Files
qinglong/docs/adr/ADR-0032-approved-action-dispatch-execution-and-start-barrier.md
T

141 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0032Approved 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` 引用 immutable `ApprovedActionDispatch`
- 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. 状态机区分副作用前和副作用后
```text
pending
-> leased
-> retry_wait -> leased
-> blocked
-> executing
-> succeeded
-> failed
-> blocked (indeterminate)
```
- `pending/retry_wait` 到期后可 claim
- `leased` 表示尚未跨越副作用 start barrierlease 过期可以安全 takeover
- `executing` 表示 start barrier 已提交,handler 可能已经产生副作用;
- `executing` lease 过期只产生有效状态 `recovery_required`,不得回到 due queue
- `succeeded/failed/blocked` 为终态;
- 只有明确发生在 start barrier 前的失败可以 retryattempt 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` 分为:
1. `inspect(dispatch)`:必须 side-effect-free,解析 immutable action plan 并返回实际 canonical action digest
2. `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/v3` Approval 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 可安全 takeoverpost-start crash 不会自动重复副作用;
- digest drift 在任何 handler 副作用前阻断;
- completion 丢失显式进入 recovery,而不是猜测成功或重新执行;
- edge 只有有界索引扫描和短事务,空闲时零内部 timer。
代价与风险:
- 每个 approved dispatch 增加一行 execution control 和多个短事务;
- `recovery_required` 需要后续 receipt/query resolver,否则需要人工处置;
- handler 必须满足 inspect 无副作用、actionRef 不可变和下游 idempotency contract
- 当前没有生产 handler,功能仍不可达。
## 未选择的方案
1. **leased 过期一律重新 execute**:无法区分副作用是否已发生,拒绝。
2. **Approval consumed 后直接同步调用 handler**:崩溃后无 durable claim/result,拒绝。
3. **把 mutable lease/result 写回 immutable dispatch**:混淆审批证据和执行控制,拒绝。
4. **handler exception 自动 retry**:异常可能发生在副作用之后,拒绝。
5. **用内存队列或进程锁去重**:重启丢失且不支持 cluster 多副本,拒绝。
6. **一个后台 timer 扫完整表**:edge 写放大和停机不可控,拒绝。
## 验证要求
- `0021` 能回填已有 dispatchschema ownership 覆盖表、索引和 migration
- Approval consume 的三表写入原子,execution insert 失败时 Approval/dispatch 回滚;
- due 查询使用稳定 keyset、固定页上限且不返回 executing/terminal
- 双 SQLite 连接对一个 due dispatch 只有一个 claim,其余看到 live lease
- expired leased 可以 takeoverexpired 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 通过;
- 生产目录不得导入或装配本切片。