mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
141 lines
8.5 KiB
Markdown
141 lines
8.5 KiB
Markdown
# 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` 引用 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 barrier,lease 过期可以安全 takeover;
|
||
- `executing` 表示 start barrier 已提交,handler 可能已经产生副作用;
|
||
- `executing` lease 过期只产生有效状态 `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` 分为:
|
||
|
||
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 可安全 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,功能仍不可达。
|
||
|
||
## 未选择的方案
|
||
|
||
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` 能回填已有 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 通过;
|
||
- 生产目录不得导入或装配本切片。
|