Files
qinglong/docs/adr/ADR-0068-receipt-first-local-run-startup-reconciliation.md
T

9.0 KiB
Raw Blame History

ADR-0068:回执优先、进程身份感知的本机 Run 启动恢复

  • 状态:Proposed
  • 日期:2026-07-20
  • 关联 RFCQL-RFC-0001 D-02、D-17、D-37、D-42、D-52、D-55、D-65、D-66、D-67、D-68
  • 关联 ADRADR-0001、ADR-0007、ADR-0040、ADR-0044、ADR-0053、ADR-0055、ADR-0056、ADR-0063、ADR-0066、ADR-0067、ADR-0069

ADR-0087 现行增量:本文的 recovery authority 已迁入 @qinglong/local-execution/recovery;其只能相对依赖同 package 的 /control 模块以及 production dependency local-process/runtime-core,恢复语义和常驻边界不变。

上下文

ADR-0067 已让 local-application 在 domain recovery 前读取真实 SQLite dispatching/running Run,并对候选或截断 fail closed。这消除了恒真 recovery summary 的假安全,但它只能阻断:一次正常任务完成后若进程在数据库终态提交前崩溃,系统仍无法消费 completion receipt;若旧任务仍在运行,也无法用 durable process identity 区分“原进程仍存活”和“PID 已复用”。长期只读阻断会把每次崩溃都变成人工修库。

本机恢复同时覆盖小型路由设备与 standalone 节点。实现不能复制 cluster claim/supervisor、创建第二 SQLite authority、扫描回执目录或安装常驻 timer;也不能因为数据库显示 running 就把可能仍有外部副作用的任务误判为 lost

决策

1. 独立、Profile-neutral 的恢复 package

新增 @qinglong/local-run-recovery。ADR-0072 后其生产依赖 runtime-core、local-process 与统一 local-execution-control;进程/回执基础设施归 local-process,完成终态策略归 control,启动恢复裁决归 local-run-recovery,依赖只能沿该方向前进,不得反向。三者都不导入 local-sqlite、local/cluster Profile、legacy、ORM 或 HTTPRepository 与候选 source 由 local-application 注入,因此同一个 LocalSqliteRunRepository 继续拥有唯一 DatabaseSync、operation queue、事务和 close fence。

@qinglong/local-application 的生产依赖固定为 adopted storage composition、local-process、local-execution 与 local Run recovery 四条窄边界。edge/standalone 基础及 adopted-storage 产物不携带进程/执行/恢复 package;只有 application 产物为完整启动、恢复与回执维护付费。

2. 每个候选严格按 receipt → process identity 裁决

候选按 Run ID 顺序、最多 256 项串行处理,证据顺序不可互换:

  1. 在 Repository 事务内读取 Run 与最新 Attempt,确认候选状态、version、active Attempt 数、executor type、callback sequence/token digest、durable handle 和 PID
  2. 只按 exact Attempt ID 计算分片路径并读取单个 completion receipt,不枚举目录;
  3. 回执通过严格 v1 codec、大小上限、UUID/时间/exit-code 字段校验,并用 constant-time SHA-256 校验 callback token、sequence 和 Attempt identity
  4. 可信回执在同一 Repository 事务内 CAS 推进 Attempt、Run 并追加各自事件,事务提交后再删除回执;
  5. 没有可信回执时,claimed 作为尚未启动的事实可原子转为 loststarting/running 只能检查 exact durable local-process identity
  6. exact identity 仍运行则保留;可信 not_running 在一次 50 msedge)或 100 msstandalone)的回执发布宽限后复查回执,再原子转为 lostinvalid handle、平台不支持或 provider unavailable 都不得推断 lost

回执优先是因为任务进程可能已结束但 launcher 正在发布完成事实;进程检查不能先于回执把这个窗口误判为丢失。宽限只使用启动路径中的一次性、可等待 timer,不创建后台循环、watcher 或 supervisor。

3. 回执与进程证据必须抗替换、严格有界

completion receipt 文件最多 4 KiB、不可覆盖发布,读取使用 O_NOFOLLOW 并拒绝 symlink。根路径必须是非根、无 NUL、绝对且有长度上限;目标路径只由受校验 Attempt ID 的固定分片生成。非法回执隔离并使该候选保持 unresolved,不把损坏事实当作任务失败。

local-process durable handle 使用 ql3lp1,绑定 Linux boot ID、PID、process group 与 /proc/<pid>/stat start ticks。PID 不存在或 exact identity mismatch 才是可信 not_running/proc 不可用、handle 畸形或 provider 异常均为 unknown/unavailable。当前 macOS 不能提供同等事实,因此携带 active local-process Attempt 时 fail closed,而不是降低证明标准。

4. 终态推进必须是聚合事务,而非散落更新

恢复事务在写入前重新读取并逐项匹配 Run ID/version/status、Attempt ID/status、callback sequence/token digest、durable handle 和 PID。可信完成按 exit code/cancellation/deadline 映射 succeeded/failed/cancelled/timed_out;可信未运行映射 lost。每次推进必须在一个 Repository transaction 中完成:

CAS Attempt + append Attempt event
CAS Run     + append Run event
commit

任一 CAS、字段复核或事件写入失败都整体回滚。恢复不创建新 Attempt、不调用 Execute、不自动 replay,也不消费 cancellation/retry 产品策略;这些动作必须在启动安全成立后由正常调度或独立人工流程决定。

5. 最终 verifier 独立复核数据库与外部事实

逐项处理后必须再次调用有界候选 source。truncated=true 永远不安全。每个仍保留的候选必须与先前保存的 Run/Attempt 指纹完全一致、没有新回执,并再次证明 exact process identity 正在运行;任何新增、消失、字段漂移、回执出现或进程身份变化都撤销本轮安全结论。

因此 application 可继续启动的条件不是“只剩零候选”,而是:所有候选已经原子终态化,或剩余候选均在最终 verifier 中被第二次证明为同一活进程。remaining 表示已知仍运行且指纹稳定的候选;unknown/invalid/provider failure 计入 failed 或 unresolved 并阻断 lifecycle/admission。

6. 低资源与集群节点边界

  • 零候选只支付一次 durable query,不访问文件系统或 /proc
  • 候选页固定最多 256,截断在读取任何 receipt/process 证据和部分 mutation 前失败;
  • 处理串行且只保留有界指纹,不创建 worker pool、第二连接或常驻内存索引;
  • 无目录扫描、轮询、watcher、周期 timer 或自动 replay
  • standalone 只放宽一次性 receipt grace,不改变证明语义;
  • cluster 节点继续使用 PostgreSQL claim/provider/fencing 模型,不能复用本机 PID 证据冒充远端 Worker 事实。

被否决的替代方案

  1. 沿用永久只读阻断:安全但不可用,正常 crash-after-exit 也只能人工修复。
  2. 先探测 PID 再读回执:扩大 launcher 发布窗口内的错误 lost。
  3. 缺 handle 或不支持 /proc 时直接 lostunknown 不是可信 negative evidence。
  4. 只更新 Run 或分两次更新 Attempt/Run:会产生不可解释的聚合裂缝。
  5. 恢复时创建新 Attempt 或重放命令:可能与仍存活的旧副作用并行。
  6. 扫描 receipt root 或启动后台 supervisor:增加闪存 I/O、常驻内存和路由设备尾延迟。
  7. 把 SQLite 细节写入恢复 package:形成第二 adapter/connection authority并阻碍后续存储演进。

影响与未完成项

正向影响:

  • 本机 application 已能自动收敛可信 completion 与可信 no-process 遗留;
  • 仍存活的 exact 进程可在双重验证后安全保留,不再要求零候选;
  • 状态、事件和聚合版本在同一 SQLite transaction 中提交;
  • edge 零候选路径不增加文件、进程或后台调度成本;
  • application production closure 保持独立、可审计且不携带 cluster/legacy ORM。

仍未完成:

  • 加密本机 Secret provider、retry、Artifact retention/read 与 HTTP admission 的纵向装配;completion/cancellation/timeout/shutdown drain 已由 ADR-0072 闭环;
  • cancellation、deadline、retry 与人工 recovery 的产品流程和 API/UI
  • 真实 Linux x64/arm64、容器 PID namespace、断电/强杀与固定物理路由设备基线;
  • 具体 scheduler/executor/admission stack、target executable 和部署 controller。

因此本 ADR 证明的是“已有可信证据时可安全恢复,证据不充分时继续阻断”,不是完整本机执行生命周期已经交付。

验证

  1. 零候选只查询一次且不读取 receipt、/proc 或安装 timer。
  2. 截断在任何证据读取和状态 mutation 前 fail closed。
  3. claimed 可原子 lost 且不探测进程;缺失/无效 starting handle 不得 lost。
  4. 可信 receipt 先于 process probe,并在一个事务内推进 Attempt、Run 和两个事件;提交后删除 receipt。
  5. exact live process 在初次与最终 verifier 均被检查且不终态化。
  6. 最终确认期间进程、数据库指纹或回执变化会撤销启动安全。
  7. ADR-0073 后 production package closure 只有十二个本机 package,不含 legacy、cluster、Worker、ORM 或额外 SQLite addon,并满足体积/RSS 硬预算。