Files
qinglong/docs/adr/ADR-0053-cluster-startup-recovery-convergence-gate.md

6.5 KiB
Raw Permalink Blame History

ADR-0053: Cluster Startup Recovery Source 与 Convergence Gate

  • 状态:Proposed
  • 日期:2026-07-19
  • 关联 RFCQL-RFC-0001 D-06、D-36 至 D-41、D-52

上下文

cluster-control 的激活顺序已经固定为 readiness → assembly → recovery → lifecycle → admission,但此前 reconcile() 的安全摘要完全由调用方提供。一个空实现可以直接返回 safe: truebootstrap 没有独立的 PostgreSQL 事实可以复核。与此同时,直接扫描全部非终态 Run 既可能拖垮大库,也会把正常 queued/running 工作误判为启动遗留,使有业务流量的集群无法滚动扩容。

本决策只补齐“恢复候选发现与恢复后收敛证明”,不伪装成已经实现了修改 Run/Attempt 的生产 Reconciler。

决策

1. Recovery Source 是 runtime-core 端口

ClusterControlRecoverySource 接受 limit,返回由 durable source 自己选择的 observedAtMs、按稳定时间/身份排序的候选页与 hasMore。页大小硬上限为 128;实现不得内部递归翻页、执行 COUNT(*)、创建 timer、持有全表结果或导入 legacy SQLite recovery。PostgreSQL adapter 必须在同一条 statement 内用数据库 statement_timestamp() 冻结 observation,Pod 本机时钟和调用方输入都不能决定 lease 是否过期。

source 通过 ClusterControlAssemblyInput.recovery 暴露给后续真实 Reconciler。它只授予候选读取能力,不授予绕过 RunRepository fence 的状态修改能力。

2. 候选只表示失去有效 ownership 的工作

PostgreSQL source 只返回:

  • 持久化停在 created 的 runtime-owned Run
  • 状态为 dispatching/running,但不存在 lease 仍晚于同一 observedAtMs 的 active Attempt 的 Run
  • 状态为 claimed/starting/running 且 lease 缺失或已经到期的 Attempt。

正常 queuedwaiting_approvalretry_waitlost 以及仍有有效 Attempt lease 的运行任务不是本副本的启动遗留,不得阻塞滚动扩容。终态 Run/Attempt 也不得进入候选。

该定义是保守的恢复入口,不是最终状态机裁决。真实 Reconciler 仍须通过 transaction、lease/fencing 和可信执行证据决定 lost、重试或终态,不能因候选出现就重放执行。

3. SQL 与索引都必须有硬读取上限

pg-0007-cluster-recovery-indexes 为 runtime-owned created/dispatching/running Run 和 active Attempt 建立部分索引;Attempt 索引以 lease expiry 开头。查询的 Run、Attempt 两个分支各自先 ORDER BY ... LIMIT limit+1,再合并并执行同一上限,因此一次调用最多从两个候选分支各取得 129 行,不会把全部 backlog 拉进 Node.js 内存。

schema capability 推进到 control-core v6,并增加 cluster_recovery: 1。reviewed migration、冻结 checksum manifest、Drizzle schema、catalog contract 和 readiness 必须 lockstepruntime role 继续只使用既有 Run/Attempt SELECT 权限,不新增 DDL 或管理权限。

4. Bootstrap 独立复核恢复摘要

调用方 application stack 先执行自己的 reconcile()。只有它返回精确的 safe=true, remaining=0, failed=0bootstrap 才使用独立 ClusterControlRecoveryConvergenceVerifierlimit=1 查询 PostgreSQL

  • 无候选且 hasMore=false:返回安全收敛;
  • 存在候选:返回 safe=falseremaining 只是至少还存在多少项的低成本下界;
  • source、数据库 observation 或 row contract 异常:抛出并让 activation fail closed。

因此调用方不能用一个恒真的摘要绕过数据库事实。未收敛或验证失败时不得启动 lifecycle、安装 /api/v3 admission 或让 /readyz 返回 ready,并按既有顺序停止 stack 和 Pool。

5. 当前仍不是完整生产恢复

本切片没有实现候选 claim、lease takeover、执行证据探测、状态推进、重试策略、告警或多副本 Reconciler lifecycle。当前默认 production application stack 仍不存在,run.get route 也仍未自动注册。只有后续恢复实现能处理所有候选并通过本 verifiercluster Profile 才能继续进入 lifecycle/admission。

被否决的替代方案

扫描所有非终态 Run 并要求清空

拒绝。queued/等待/有效 lease 都是正常集群状态;这种门禁会使活跃集群无法扩容,并把启动时间绑定到业务 backlog。

COUNT(*) 返回精确 remaining

拒绝。激活只需要知道是否存在遗留。精确计数会在最坏时扩大数据库扫描和锁压力;低成本下界足以 fail closed,精确 backlog 应由异步指标维护。

信任 application stack 返回的 safe: true

拒绝。composition 回调不是数据库权威,空实现、遗漏状态或测试替身都可能在恢复未完成时开放 admission。

在 bootstrap 内直接把过期 Attempt 标记 lost

拒绝。候选发现不等于拥有状态推进 authority;未经 lease/fence、执行证据和 retry policy 裁决直接改状态可能制造重复执行或覆盖其他副本。

影响

正向

  • 激活门首次能从 PostgreSQL 独立证明恢复摘要不是恒真占位;
  • 正常活跃工作与真正失权遗留被明确区分,支持滚动扩容;
  • 单次查询、单页内存和索引范围都有硬上限;
  • edge/standalone 不引入 PostgreSQL source、额外 timer 或 cluster 依赖;
  • capability/readiness 可以拒绝缺少恢复索引的旧 schema。

代价与未完成项

  • 新增两个部分索引及 capability v6 migration
  • source 只提供候选,真实多副本恢复 claim/supervisor 仍需后续 ADR
  • lease writer 必须使用与 PostgreSQL observation 兼容的数据库时间语义,生产仍应监控数据库节点间时钟漂移;
  • 需要 PostgreSQL 16/18 EXPLAIN/大 backlog 基准证明查询计划稳定使用部分索引。

验证

  1. runtime-core contract test 证明 verifier 固定只请求一项并拒绝不一致页/非法 durable observation。
  2. PostgreSQL adapter test 证明两个分支和合并层都使用 limit+1,并拒绝越界 limit、终态或畸形 row。
  3. migration/manifest/schema/readiness tests 证明 capability v6、两个部分索引与冻结 checksum lockstep。
  4. bootstrap test 证明 application 返回 false-safe 时仍不能安装 admission,并按 stack → Pool 关闭。
  5. 正常 queued、等待状态和有效 lease 不进入 SQL 候选条件;过期或缺失 lease 才进入。
  6. Node 22/24、PostgreSQL integration、edge import audit、cluster dependency audit 与 GitNexus detect-changes 继续作为合并门禁。