Files
qinglong/docs/adr/ADR-0055-cluster-recovery-evidence-and-fenced-lost-transition.md
T

9.4 KiB
Raw Blame History

ADR-0055: Cluster Recovery Evidence 与 Fenced Lost Transition

  • 状态:Proposed
  • 日期:2026-07-19
  • 关联 RFCQL-RFC-0001 D-03、D-06、D-18、D-25、D-26、D-42、D-52 至 D-54

上下文

ADR-0053/0054 已把失效 ownership 候选、有界 discovery、独立 recovery claim 和多副本 fencing 建立起来,但“取得 claim”只证明当前副本可以裁决,不能证明任务已经停止,更不能证明任务可以安全重放。尤其是 starting/running Attempt:执行 lease 过期只能证明控制面失去 authority,Worker、容器或外部执行器仍可能继续产生副作用。

另一方面,claimed Attempt 尚未越过 start barrier。只要持久化状态仍是 claimed 且 lease 已按数据库时间过期,就存在控制面内的 durable pre-start 证据,可以安全地把旧 Attempt 收敛为 lost,无需调用外部 provider。

本决策只补齐 recovery 的证据分类和 lost 状态推进,不创建下一次 Attempt、不排队、不调用 Executor,也不裁决成功、失败、取消或重试策略。

决策

1. 候选、claim、证据与 mutation 是四个独立边界

一次处理固定按以下顺序进行:

  1. ADR-0053 source 只发现失去有效 ownership 的候选;
  2. ADR-0054 repository 只授予短期、可接管的 recovery claim
  3. evidence processor 在 claim transaction 外重读 Run/Attempt,并只在必要时查询可信 provider;
  4. resolution repository 重新锁定完整 claim fence、重读聚合并原子提交状态与事件。

任何一步都不得把前一步的结论扩大为任务执行 authority。外部 provider 调用期间不持有数据库连接、transaction 或行锁。

2. 证据矩阵必须保守且可枚举

持久化事实或外部证据 自动结果 理由
claimed、lease 已过期、未越过 start barrier Attempt/Run lost durable pre-start 事实证明没有被允许启动,不需要外部探测
starting/running、可信 provider 返回 not_running Attempt/Run lost 精确执行身份的可信负证据证明当前执行不存在
starting/running、provider 返回 running bounded retry 旧执行仍可能产生副作用,不能写 lost 或重放
provider provider_unavailable bounded retry 暂时不可观察不等于任务不存在
provider identity_unverifiableconflicting_evidence manual 身份不完整或证据冲突不能靠自动重试消除歧义
execution lease 已恢复有效 resolved 当前快照不再是 recovery 候选
cancellation intent 未收敛 bounded retry recovery 不越权替代 cancellation coordinator
created Run、缺失 Attempt、Run/Attempt 终态矛盾或未知组合 manual 聚合不满足安全自动转换前提

provider 必须接收绑定 Run/Attempt、Attempt 状态、executor type、Worker/handle/PID、execution lease 的精确目标。它只能返回 running | not_running | unknown(reason),不得执行 start、stop、retry、completion callback 或任意外部 mutation。

3. Lost 转换权限必须窄于通用状态机

runtime-core 只提供 recovery 专用的纯 lost 转换:

  • 仅接受 executionOwner=runtimelegacy owner 不可达;
  • cancellation intent 存在时拒绝转换;
  • claimed/starting/running Attempt 才能转 lost
  • dispatching/running Run 才能随 Attempt 转 lost
  • lost Attempt 可驱动仍 active 的 Run 补齐 lost
  • 保留原 execution lease、Worker、handle、PID 和 callback sequence 作为审计证据;
  • 按顺序产生 attempt.lostrun.lost,每步独立推进 Run version/event sequence,并使用稳定 dedupe key。

该 primitive 明确不创建 Attempt N+1、不改变 RetryPolicy、不推断 terminal completion、不调用 Executor。自动重试仍必须由 ADR-0026 的冻结策略、安全性声明和新 Attempt 协议负责。

当前 legacy transitionRuntransitionRunAttempt 与版本冲突链路具有 CRITICAL blast radius。为避免以 cluster 恢复切片暗中重写 2.x 状态机,本阶段不修改这些符号;将来只有在共享 contract、调用方迁移和回退门禁齐备后,才通过独立 cutover ADR 合并实现。

4. PostgreSQL mutation 必须在完整 fence 下原子提交

applyLost() 使用短 READ COMMITTED transaction

  1. (target kind, target id, state=claimed, owner, token, version) 和 PostgreSQL statement time 校验 claim 未过期,并 FOR UPDATE 锁定 control row
  2. 在锁内重读 Run 与精确/最新 Attempt
  3. 与 provider 前快照比较 status、version、event sequence、ownership、cancellation、执行身份、lease、callback sequence 和关键时间;
  4. 使用 Run/Attempt CAS 提交每个状态转换,并在同一 transaction 追加对应事件;
  5. 任一 CAS、事件或存储步骤失败时回滚全部业务状态。

旧 owner、过期 claim 或 takeover 必须在读取和修改 Run/Attempt 前返回 fenced。快照变化返回 stale,由后续 discovery/verifier 重新裁决,不能套用旧证据。

业务 mutation 与 ADR-0054 的 control settle 故意不是一个 transaction:外部 processor 崩溃可能发生在 mutation 已提交、claim 尚未 settle 之间,但底层 Run/Attempt 已不再满足原候选,后续 verifier/discovery 会安全收敛;反向顺序会制造“claim 已 resolved、业务事实未落库”的危险窗口。

5. Profile 和资源边界不变

该 processor 位于无 driver 的 runtime-corePostgreSQL transaction 实现只存在于 @qinglong/cluster-postgres,并由 cluster-control assembly 显式注入。edge/standalone 的依赖树、启动 importer、SQLite lifecycle 和资源预算不得因此引入 pg、Drizzle PostgreSQL、control table 或 cluster supervisor。

集群节点通过小页、短 transaction、页内串行和每项 claim fencing 控制单副本成本,再依靠多副本横向处理;不得用单个无界 leader loop、全表常驻缓存或跨 provider I/O 的长 transaction 换取吞吐。

6. 当前仍不开放生产 recovery

ADR-0056 已增加 exact-type、identity-declared、timeout/single-flight 有界的 production registry,并由 cluster bootstrap 默认拥有;但仍没有 Remote Worker、Kubernetes 或 Container 的真实认证 provider。LocalProcess、Remote Worker、Container 与 Kubernetes 必须分别证明什么是“精确身份”和“可信 not-running”,不能用通用 process missing、网络超时或 Worker offline 冒充负证据。

默认 startup 多轮 lifecycle、取消协调、lost 后 retry PostgreSQL adapter、指标/告警、manual recovery API/UI 和强认证审计也仍未完成。因此 cluster-control 仍不能因为本 ADR 的集成测试通过就宣称具备生产自动恢复或开放 mutation admission。

被否决的替代方案

Lease 过期一律写 lost 并自动重跑

拒绝。starting/running 已越过 start barrier,lease 只证明失权,不证明副作用停止;重放会产生 split-brain 和重复业务写。

Provider 不可用时按 not-running 处理

拒绝。观察系统故障与执行不存在是不同事实;前者只能 bounded retry,身份不可验证或证据冲突必须进入 manual。

外部探测后直接调用通用 Repository 多次写入

拒绝。探测期间 Run、Attempt 或 claim 都可能变化,缺少重新 fencing、快照比较和单 transaction CAS 会留下半转换聚合或让旧 owner 晚到覆盖。

Recovery processor 直接创建 retry Attempt

拒绝。lost 判定与 retry eligibility 是两种 authority。后者还需要冻结策略、次数预算、幂等/去重声明和 Attempt N+1 原子创建,不能由 evidence provider 决定。

影响

正向

  • pre-start 与 post-start 使用不同证明标准,不把 lease expiry 当成统一死亡证明;
  • 多副本旧 owner 在 probe 后仍无法越过 PostgreSQL fence
  • Run/Attempt/双事件要么全部提交,要么全部回滚;
  • execution identity 与 lease 证据被保留,便于后续人工取证;
  • runtime-core contract 可被不同 provider/数据库复用,同时保护 edge 安装与启动闭包。

代价与未完成项

  • starting/running recovery 依赖每种 Executor 的可信 evidence provider
  • 暂时故障会保守 retry,歧义会进入 manual,可能延长 admission 阻塞;
  • 状态 mutation 后、control settle 前存在可恢复窗口,需要 verifier 与指标证明最终收敛;
  • 通用 legacy 状态机仍有重复实现,必须在单独高风险 cutover 中消除;
  • 仍需 PostgreSQL 16/18 多 Pool、failover、provider 延迟/故障注入和大 backlog 资源基准。

验证

  1. runtime-core contract test 覆盖 claimed 不调用 provider、可信 not_runningrunning/provider unavailable retry、身份不可验证/证据冲突 manual、有效 lease/stale/fence 和纯转换版本/事件语义。
  2. PostgreSQL unit test 覆盖完整 control lock fence、快照重读、Run/Attempt CAS、双事件顺序、部分 CAS 回滚,以及失权后不读取/修改 Run。
  3. PostgreSQL 13.3 真实集成测试证明未启动 aggregate 在无 provider/无重放下原子 lostprovider 返回后 claim 被另一副本接管时旧 owner 被 fenceRun/Attempt 保持不变。
  4. Node 22/24 package test、cluster assembly test、edge import audit、cluster dependency audit 与 GitNexus detect-changes 继续作为本地门禁;正式支持结论仍以 Node 24.18+ 和 PostgreSQL 16/18 CI 为准。