Files
qinglong/docs/adr/ADR-0054-cluster-recovery-claim-fencing-and-bounded-supervisor.md
T

105 lines
7.1 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-0054: Cluster Recovery Claim、Fencing 与有界 Supervisor
- 状态:Proposed
- 日期:2026-07-19
- 关联 RFCQL-RFC-0001 D-06、D-36 至 D-42、D-52、D-53
## 上下文
ADR-0053 已能从 PostgreSQL 有界发现失去有效 ownership 的 Run/Attempt,并在 admission 前独立验证是否收敛,但候选没有多副本 ownership。若两个 cluster-control Pod 都读取同一个过期 Attempt 并直接处理,它们可能重复探测、互相覆盖结果,甚至重放任务。把 `FOR UPDATE SKIP LOCKED` 事务一直持有到外部证据探测结束也不可接受:慢 Worker、网络或 Artifact I/O 会占用连接和行锁,放大集群故障。
本决策增加恢复协调 authority,不增加任务执行 authority。ADR-0055 已补充 narrowly scoped 的 evidence processor 与 fenced lost transition;其他终态、重试、取消和生产 provider 仍由后续专用组件定义。
## 决策
### 1. 恢复 claim 使用独立控制记录
`pg-0008-run-recovery-claims` 新增 `ql3.run_recovery_controls`。每个 `run|attempt` 候选只有一行控制记录,保存候选快照、数据库 observation、`available|claimed|retry|manual|resolved` 状态、owner、token、单调 version、claim expiry、next claim time 和有界 failure count。
该表不复用 RunAttempt 的执行 lease。执行 lease 是任务执行 authority 和原始故障证据;恢复 claim 只是“哪个控制面副本可以裁决此候选”的短期 authority。覆盖执行 lease 会销毁取证边界并混淆 Worker 与 recovery owner。
### 2. Claim 事务必须短,processor 在事务外运行
一次 `claim()` 最多请求 128 项,并在一个短 `READ COMMITTED` transaction 内完成:
1. 用 ADR-0053 source 在同一数据库连接读取固定 observation 的有界候选页;
2. upsert 候选快照,但不覆盖已有 claim/retry/manual/resolved 控制状态;
3. 只在本页候选内按稳定顺序选择可认领、retry 已到期或 claim 已过期的行;
4. 使用 `FOR UPDATE SKIP LOCKED` 更新 owner/token/version/expiry 后立即提交。
外部证据探测、Run/Attempt 重读和后续状态机处理必须在事务外执行。不得跨网络调用、Executor probe 或 Artifact I/O 持有数据库锁。
### 3. Settle 必须被完整 fence
`settle()` 是单条原子 UPDATE,必须同时匹配 `(target kind, target id, state=claimed, owner, token, version)`,并由 PostgreSQL `statement_timestamp()` 证明 claim 尚未过期。任一条件不匹配返回 `fenced`,旧副本不得覆盖接管者结果。
允许的 disposition 只有:
- `resolved`:本次证据处理已收敛,但只要底层候选事实仍存在,后续 discovery 可以再次认领;
- `retry(delay)`:使用数据库时间写入有上限的下次认领时间;
- `manual`:永久退出自动 claim,等待后续受审人工恢复入口。
processor 异常不会被解释为 resolvedSupervisor 将其收敛为有界 retry。存储错误使用稳定、低敏且 retryable 的 `ClusterControlRecoveryStoreError`,不泄漏 SQL/连接信息。
### 4. Supervisor 单页、串行、无 timer、绝不执行任务
runtime-core 的 `ClusterControlRecoverySupervisor` 每次只处理一个 claim page,默认 16、硬上限 128claim lease 默认 30 秒、硬上限 5 分钟;retry delay 硬上限 5 分钟。页内串行处理,不递归翻页,不创建 timer,不缓存全局 backlog,也不调用 Executor。
这些边界同时服务两类部署:小规格控制节点可选择更小的 page/cadence,集群副本依靠 PostgreSQL claim/fencing 横向并行。edge/standalone 不安装该 PostgreSQL 表、driver 或 supervisor,它们继续使用 Profile 专属 SQLite 生命周期。
### 5. Capability 与最小权限 lockstep
control-core capability 推进到 v7,并增加 `cluster_recovery_claim: 1`。reviewed migration、冻结 checksum、Drizzle schema、catalog contract 和 readiness 必须同步。runtime role 对控制表只有 SELECT/INSERT/UPDATE、没有 DELETE/DDLcluster-admin 对该表零权限。
`ClusterControlAssemblyInput` 同时暴露只读 `recovery` source 和 `recoveryClaims` repository,避免把候选发现、协调 authority 和业务状态推进揉成一个万能 Repository。
### 6. 当前仍不开放生产执行
ADR-0055 已实现 provider contract、保守 evidence processor 与 Run/Attempt `lost` 原子事务,但仍没有默认 production evidence provider、retry/其他 terminal 状态事务、周期 lifecycle、指标告警或人工 recovery API。bootstrap 仍只接受 application 提供的 reconcile,并在之后独立验证 source 收敛。
因此不能把 claim 成功解释为任务可安全重跑,也不能据此默认注册 `run.get`、开放 mutation API 或宣称 cluster-control 已可部署。
## 被否决的替代方案
### 复用 RunAttempt execution lease
拒绝。它会覆盖原 Worker authority 和过期证据,使 recovery 与执行 ownership 无法独立审计。
### 在外部证据探测期间保持行锁
拒绝。慢依赖会形成长事务、连接耗尽和锁放大;Pod 崩溃只能依赖数据库断开,无法表达可观测 retry/manual 状态。
### 只使用进程内 mutex 或 leader election
拒绝。进程内锁不能协调多 Pod;单 leader 会把全部 backlog 和 failover 恢复压到一个节点,而且仍需要每项 fencing 防止旧 leader 晚到写入。
### Claim 到期后直接重放任务
拒绝。claim expiry 只证明 recovery owner 失权,不证明旧任务副作用不存在。自动重试必须经过固定 Run 策略、Attempt N+1 和可信业务去重边界。
## 影响
### 正向
- 多副本对同一候选只有一个 claim winner,过期后可接管;
- 旧 owner 的迟到 settle 被数据库时间、token 和 version fence 拒绝;
- 外部探测不持有数据库事务或行锁;
- 单批数据库、内存和处理工作量均有硬上限;
- manual/retry/resolved 具备持久化、可审计控制状态;
- runtime/admin 权限边界可由 readiness 精确验证。
### 代价与未完成项
- 新增一张控制表、三个部分索引和每候选一次持久化控制写;
- resolved 只表示控制处理结果,底层候选未消失时会再次被发现;
- poison candidate 仍可能反复消耗页预算,需要后续告警、退避策略和人工恢复入口;
- 仍需 PostgreSQL 16/18 多 Pool、failover、大 backlog `EXPLAIN` 与资源基准。
## 验证
1. runtime-core contract test 证明 Supervisor 单页串行、异常转 retry、manual/retry/fenced 均保持 unsafe,并拒绝越界配置与畸形页。
2. PostgreSQL unit test 证明短事务顺序、rollback/release、完整 settle fence 与稳定低敏错误。
3. PostgreSQL 13.3 真实集成测试证明两个 repository 并发只有一个 winner、有效 claim 不可偷取、到期接管后旧 settle 被 fence、retry 未到期/manual 不可认领、resolved 在底层事实仍存在时可再次认领。
4. migration/manifest/Drizzle/schema/readiness test 证明 capability v7、表、索引、CHECK/FK 和 runtime/admin 权限 lockstep。
5. Node 22/24、PostgreSQL 16/18 三角色 CI、edge import audit、cluster dependency audit 与 GitNexus detect-changes 继续作为合并门禁。