Files
qinglong/docs/adr/ADR-0310-docker-target-start-restart-barrier.md
T

8.5 KiB
Raw Blame History

ADR-0310Docker Target 启动/重启屏障与人工终态

  • 状态:Accepted
  • 日期:2026-08-09
  • 关联 RFCQL-RFC-0001 D-05、D-17、D-63、D-64、D-65、D-259
  • 关联 ADRADR-0065、ADR-0243、ADR-0308、ADR-0309

背景

ADR-0309 已让 deployment owner 以真实 Docker controller 把 2.x owner 收敛为 legacy_stopped,并让 adopted Application v3 在取得 signal、SQLite、Secret、Plugin Package、AI 和 lifecycle authority 前消费该 commitment。但“旧实例已经停止”不等于“可以用一条无状态 shell 命令反复启动 3.0”:若 controller 在 Docker start 已产生副作用后崩溃,自动重试可能重复启动;若 target 曾 active 后旧实例被人工拉起,直接重启 target 又可能形成双 owner。

已删除的 ADR-0065 孵化包记录过正确状态语义,但它没有 production consumer。该语义必须进入现有 ql3-local-deploy 产品,而不是恢复第二十个 workspace package。低配路由器也不能为此安装 daemon、 watcher、数据库连接或进程管理 framework。

编辑前 GitNexus 显示部署 CLI main 为 LOW、1 个直接文件调用和 0 条产品执行流;新 supervisor 建立后, 重放入口为 LOWjournal path/read helper 为 MEDIUM、最多 6 个包内直接调用者且仍为 0 条产品执行流, 没有 HIGH/CRITICAL 风险。

决策

1. 继续使用现有 deployment product

新增两个显式命令,但不新增 package:

ql3-local-deploy cutover-target-start
ql3-local-deploy cutover-target-restart

二者只存在于 @qinglong/local-owner-cli/deployment/cutover。Application 不导入 Owner CLI、Docker、 journal writer 或 controllerCluster/Worker/PostgreSQL 产物也不取得本机 Docker authority。

2. Target identity 在 start 前冻结

命令只接受完整 64 hex 的 legacy/target container ID 和不可变 repo@sha256 target image。首次启动或重启 前,controller 必须同时证明:

  • target 为 created|exited|deadRunning=falseRestarting=falsePaused=falsePid=0
  • restart policy 为 noroot filesystem 只读、非 privileged 且启用 no-new-privileges
  • Cmd 精确读取受审 Application v3 config
  • 当前 UID 私有 config 仍绑定 cutover/Profile/instance/activation/legacy commitment
  • 一个且仅一个读写 bind mapping 能分别解释 config、commitment、activation 和 legacy source 在 target 内的路径;
  • container Created/Image/Name/ID、config digest 和 mount mapping 形成低敏 digest,随后不可漂移。

deployment controller 不把 container name、短 ID、PID、operator boolean 或单独的 Docker exit code当作身份。

3. 每代 start 只能跨越一次持久屏障

首次 generation 固定为 1

0001 legacy_stop_requested
0002 legacy_stopped
0003 target_start_requested | manual_required
0004 target_active          | manual_required

0003 先通过现有 hard-link no-replace primitive 原子发布,再调用一次 docker container start <exact-id>。若 controller 在屏障后崩溃,原命令重放只能 inspect,不能再次 start。 只有同一 target identity/binding 已 running,并且 Application 写出了新的、校验通过的 Linux startup receipt 才发布 target_active

start 响应失败或丢失也不直接等于失败:controller 在 Edge 30 秒/120 次、Standalone 60 秒/240 次的有界窗口 内只做 inspect。窗口结束仍不能证明 active 时发布 terminal manual_required,错误正文不入 journal,只保存 有限 reason、uncertain state 和 domain-separated error digest。

4. 每次 restart 都重新证明 Legacy 静默

generation 2..15 固定追加四条:

legacy_recheck_requested
legacy_reverified | manual_required
target_restart_requested | manual_required
target_active | manual_required

Legacy inspect 必须重新得到与 0002 相同的完整 container identity digest 和 source bind digest,并再次证明 stopped + restart=no。只有 legacy_reverified 的 record digest 才能成为 restart request 的直接前驱;即使攻击者 重算后段 JSON 自身 digest,只要把它从该前驱链拆开仍会失败关闭。

restart 前 target 必须停止、identity/binding 不漂移,磁盘上的 startup receipt 必须等于上一代 target_activerestart 后必须出现不同 receipt。这样 supported controller 路径不会把旧 active receipt 误认成新进程,也不会跳过 Legacy recheck。

5. manual_required 不可自动离开

本批支持四个有限 reason

legacy_silence_unproved
target_preflight_unproved
target_start_result_unproved
target_restart_result_unproved

同一 generation 的 decision/outcome 使用固定中性文件名,因此 requested/active 与 manual 两个并发 writer 竞争同一个 no-replace inode,不会各自在不同文件中“双赢”。终态原命令只验证磁盘链并返回 existing,不重新 打开 Docker socket。任何路径都不自动启动 2.x。

6. 资源与源码结构

该能力是一条人工触发的一次性命令:没有常驻 timer、watcher、listener、daemon、数据库连接或目录历史扫描; 每代固定最多四个小 JSON,最多 15 代/60 条 target 记录,沿用 cutover catalog 的 64 项上限。轮询仅存在于 启动命令生命周期中。

实现没有形成新的顶层 package,也没有把所有逻辑塞进一个根文件。cutover/ 内部分为 target command contract、Docker/application evidence、journal primitive、record evidence 和 supervisorworkspace 根源码 仍只有公共/二进制入口。

被否决方案

  1. start 失败后直接重试:失败可能发生在 Docker 已完成副作用之后,拒绝。
  2. 只看容器 running:无法证明正确 Application 已取得并报告 active,拒绝;必须同时消费新 startup receipt。
  3. 复用上一代 Legacy commitment 直接 restart:旧实例可能已被人工拉起,拒绝;每代重新 inspect。
  4. 让 Docker unless-stopped 自动恢复 adopted target:会绕过 restart generation 和 Legacy recheck,拒绝; target 必须 restart=no
  5. 为 supervisor 新建 workspace package:没有新的 artifact/dependency/consumer 边界,拒绝。
  6. 让 Application 调 Docker 或写 deployment journal:会把常驻 runtime 提升为部署 owner,拒绝。

验收证据

  • 新专项 6/6:正常首次启动与无 Docker 重放、屏障崩溃 inspect-only、未知 start 进入 manual、restart 前 Legacy reproof、Legacy 漂移禁止 restart、自洽但断链的 restart record 失败关闭。
  • @qinglong/local-owner-cli 完整沙箱外回归 116/116;沙箱内唯一失败是既有 provider test 不能监听 127.0.0.1 的 EPERM,沙箱外原样通过。
  • Application 43 项为 40 pass/3 条件 skippackage boundary 8/8、dependency 48/48、完整 19-package clean build/test 与 Backend 1,1141,112 pass/2 条件 skip)通过。
  • workspace 为 781 source/32 root/749 nestedOwner CLI 为 56/1/55package strict TypeScript/closure check 通过,workspace package 仍为 19,不新增 production dependency。
  • 四个常驻 Application 制品仍 compatibleEdge/Standalone 为 4,744,898/4,745,042 bytes、432 files、 110 modulesAI 两档为 6,132,511/6,132,667 bytes、496 files、109 modules;最大实测 RSS delta 21,528,576 bytes,低于 24 MiB 门,Owner controller 未进入闭包。
  • 最终 GitNexus 为 43,641 nodes/99,242 edges/1,711 clusters/272 flows;主入口和 evidence parser 为 LOW journal read/path 为 MEDIUM、5 个包内直接调用者、0 process;detect_changes all/compare develop 仍为 12 files/31 symbols 与 14/34、low/0 process。QL3 孵化树未完整进入 Git baseline,因此 diff graph 只作 补充,不替代逐符号 impact、运行测试与 artifact 门。
  • 本批未改 SQL、migration、PostgreSQL/Cluster runtime 或部署资源,不重复生成 PostgreSQL HA 物理晋升证据。

未完成

  • systemd/OpenRC legacy 与 target controller
  • manual_required 的只读诊断、双确认 resolution 与新 cutover ceremony(后续已由 ADR-0313 关闭);
  • target 显式 stop 与最小写后分类(后续已由 ADR-0314 关闭;数据域 reconciliation/rollback 仍未完成);
  • adopted Compose target 的受审 create/config 生成器及真实 Docker live/crash gate
  • Cluster/Kubernetes 独立 cutover authority。

因此本 ADR 关闭 Docker target start/restart 的产品状态机与保守终态,不宣称 D-64 或 QingLong 3.0 整体完成。