6.7 KiB
ADR-0448:有界 Legacy Shadow 启动恢复
- 状态:Accepted
- 日期:2026-08-18
- 关联 RFC:QL-RFC-0001 D-02、D-356、PR-4
- 关联 ADR:ADR-0001、ADR-0002、ADR-0445、ADR-0446、ADR-0447
- Amends:ADR-0002 的 Shadow 启动与恢复边界
- Amended by:ADR-0449
上下文
Legacy Shadow 已能旁路观察同一 ChildProcess、关联跨 worker callback/stop,并为 system crond 提供 response-loss-safe execution ID;但 HTTP worker
重启后,原 worker 的内存注册表和 ChildProcess listener 都已消失。数据库可能留下 queued/dispatching/running 的 legacy-owned Run,而 Legacy
initData 又会在 HTTP 启动期间先把旧 RunningInstance 从 running 归一为 stopped。
因此不能照搬 Primary Reconciler:Legacy Attempt 没有可复验的 durable Executor handle,PID 可能重用,system crond 还可能独立于 HTTP worker 继续运行。仅用 PID 存活检查会伪造 owner 连续性;把所有非终态 Run 一律 lost,又会提前终结仍可通过稳定 callback 收敛的 system crond。
部署范围同时覆盖低性能路由设备、standalone 和 cluster 节点。启动恢复不得引入常驻 watcher、第二个 SQLite authority 或对 cluster profile 的本机 数据库误装配。
决策
- 只有
QL3_SHADOW_ORIGINS至少显式启用一个已审 origin 时,HTTP worker 才加载恢复 Source、Run Repository、Writer 和 Reconciler。默认 off 不导入重型 adapter、不查询数据库,也不创建 timer/listener。 - 恢复发生在 Legacy
initData完成 RunningInstance 状态归一之后、manual Primary activation 与 HTTP listen 之前。这样启动期间没有新的 HTTP manual admission 与恢复扫描竞争,Primary 也不会先于 Shadow 遗留事实审计激活。 - 新的只读 Source 使用
(created_at_ms, run_id)keyset 分页,只扫描 enabled origin、legacy owner 且状态为 queued/dispatching/running 的 Run。单页硬上限 64;每个 Cron 的 RunningInstance 证据硬上限 8,超限、重复身份或冲突一律 ambiguous,不猜测更新。 - Source 只把 RunningInstance 的 log path 转换为现有 36 字符 opaque log artifact ID;原始路径、command、用户名、Run ID 和 Cron ID 不进入 audit message 或指标 label。
- 状态裁决如下:
- 唯一 PID/log/唯一实例证据已经 finished/error:复用 Shadow Writer 补齐 succeeded/failed;
- 唯一 stopped 且带 finished time:补齐 cancelled,reason 为 reconcile;
- queued + claimed 且没有 spawn 证据:以 reconciler actor 收敛为 cancelled,并记录 acceptance abandoned;
- dispatching/running 的 Node worker-owned origin 在重启后没有终态证据:Attempt/Run 收敛为 lost;
- scheduled_system 没有终态证据:保持 pending,等待稳定 execution ID callback,不能因 HTTP worker 重启提前终结外部 crond;
- 多 Attempt、证据截断、身份冲突或非法状态:只计 ambiguous/failed,不覆盖 Legacy UI 状态。
- lost/abandoned 的 Attempt 与 Run 使用两个既有原子命令事务推进。若 Attempt 已提交而 Run 响应丢失或进程退出,下次启动会从唯一 terminal Attempt 修复 active Run;不产生第二个 Attempt,也不重放外部副作用。
- Reconciler 每次 HTTP 启动只运行一次:edge 为
8 × 1 page,standalone 为32 × 4 pages。页预算耗尽返回稳定 resume cursor 和remaining=true,不在进程内排队、不自动循环;后续差异报表和正式 Primary gate 必须把 remaining/ambiguous/failed 视为未闭合证据。 cluster-control与workerprofile 拒绝本机 Legacy Shadow 恢复装配。它们未来必须使用 PostgreSQL/shared authority 的独立 Reconciler,不能复用 Legacy SQLite。- Source、写入、配置或 audit sink 失败均保持 Shadow fail-open,只输出低敏 error type/有界 summary,不阻止 2.x HTTP 服务启动。
资源与部署影响
- 不新增 package、生产依赖、schema、migration、表、索引、进程、线程、端口、timer、watcher 或 Kubernetes 对象。
- edge 每次启动最多扫描 8 个 Run,每个带 Cron identity 的候选最多读取 8 条 RunningInstance;standalone 最多扫描 128 个 Run。
- 查询使用现有 Runs/RunAttempts 索引与 keyset,不用 OFFSET、不全表加载 Attempt,也不把原始日志路径带出 adapter。
- 默认 off 和 cluster/worker profile 都是零 Repository、零恢复查询、零写入;只有显式 Shadow 的本机 profile 承担一次性启动成本。
被拒绝的替代方案
复用 Primary Startup Reconciler
拒绝。Primary 依赖 durable Executor handle、receipt journal 和 runtime owner;Legacy Shadow 不具备这些证据,复用会把 PID 猜测伪装成精确身份。
启动后定时全表扫描
拒绝。它会给路由设备增加常驻 timer、重复数据库唤醒和不可控历史扫描,也与当前一次性启动门边界不符。
所有旧 active Run 一律 lost
拒绝。system crond 独立于 HTTP worker,并可用稳定 execution ID 在重启后补发终态 callback;提前 lost 会丢弃更强事实。
在 Source 中复用原始 log path 作为跨层 identity
拒绝。原始路径可能暴露任务结构或用户信息;adapter 内必须先转换为现有 opaque artifact ID。
在 cluster profile 打开同一 SQLite 恢复器
拒绝。多节点会形成多个本机 authority,既无法看到共享事实,也可能产生冲突终态。
验证
- 真实 SQLite 覆盖唯一 RunningInstance 成功终态、startup-reset lost、spawn 前 abandoned、显式 stopped、system-crond pending、重复身份拒绝、 terminal Attempt response-loss 修复和稳定 keyset 分页。
- Bootstrap 合同覆盖默认关闭零 execute、cluster/worker 拒绝、edge/standalone 独立预算、低敏失败开放,以及
Legacy normalization → Shadow recovery → Primary activation → HTTP listen顺序。 - 聚焦测试
15/15;Legacy/Shadow 扩展63/63,Legacy 身份专项8/8,两者串行组合71/71;完整 backend1,434 pass / 0 fail / 2 conditional skip;18 个 QL3 package 均完成 clean build/test,build:back通过。 - 14/14 static audit 与 14/14 artifact audit 通过。edge/standalone 的 base、adopted、application、application-api、AI、
application+AI、MCP 产物分别为
2,589,998 / 2,590,076、2,809,293 / 2,809,416、3,632,877 / 3,632,997、3,800,430 / 3,800,574、3,069,251 / 3,069,341、4,493,151 / 4,493,283、7,315,930 / 7,316,038bytes,与前一阶段一致。 - 本切片不改 schema/migration、数据库连接拓扑、容器或 Kubernetes 资源,未重跑物理 PostgreSQL HA/K3s 门;对象存储 backup/WAL/restore/PITR 与 cert-manager mTLS 轮换继续保留为发布最终化现场证据门。