Files
qinglong/docs/adr/ADR-0199-generation-fenced-local-compose-rollout.md
T

129 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0199Generation-Fenced 本机 Compose Rollout
- 状态:Accepted
- 日期:2026-07-29
- 关联 RFCQL-RFC-0001 D-184、D-187、D-188、D-189
- 关联 ADRADR-0194、ADR-0197、ADR-0198
## 背景
ADR-0198 能证明本机 exact digest、当前 SQLite 和 Compose 合并结果兼容,但
`docker compose up -d` 的成功只表示 daemon 接受了创建请求,不表示应用完成存储、
Secret、Scheduler、Executor 和 Plugin Package recovery 激活。Docker 与 SQLite 也
不存在可同时提交 selection、容器状态和部署回执的事务。
因此 rollout 必须显式处理四个窗口:preflight 后 generation 被并发切换、候选容器
启动后没有 active、rollback selection 已提交但容器尚未恢复,以及 receipt 已落盘
但调用方没有收到响应。
## 决策
### 1. 复用现有部署 CLI,不拆新 package
入口为:
```text
ql3-local-deploy compose-apply --command-file /absolute/private-command.json
```
command 固定包含 deployment root、canonical Docker executable、explicit canonical
Unix socket、root acknowledgement、expected generation、rollout UUID、开始时间和
一组预授权的 failure rollback mutation/time。CLI 不接受 shell command、Compose
额外参数、健康字符串、image、container ID 或 caller 声明的 compatibility。
Docker 调用复用一个部署包内部 runner:显式 `--host unix://...`、一次性 `0700`
空 config、清理 proxy/context 环境、bounded output/timeout。该实现仍属于已有
`local-owner-cli` 的部署内聚模块,不形成新 workspace package 或常驻进程。
### 2. Rollout lock 与 revision CAS 双向 fencing
每次 apply 先以 normalized command 的 exact bytes 获取
`service/.compose-rollout.lock`。普通 `compose-revision` 在该锁存在时失败关闭;
只有同一 apply 的失败恢复可携带 exact lock intent 推进 rollback generation。
apply 获取锁后再次检查 active selection 与 immutable revision。正常路径只接受
expected generation;恢复路径只接受 `expected+1`、previous 指回 attempted
generation、rollback target 等于 attempted previous,且 mutation 等于 command
预授权值。其他状态均不能猜测。
### 3. Active 是应用事件,不是容器 Running
成功 apply 固定使用两个正式 Compose 文件,并增加:
```text
--detach --force-recreate --no-build --pull never --remove-orphans
```
随后只观察新 service container,逐项复核:
- container ID 与 Running 状态;
- exact `Config.Image`
- generation/mutation labels
- read-only rootfs、network none、非 privileged
- 应用 stdout 中 schema v1、正确 Profile、AI excluded 的结构化 `active` 事件。
Edge 窗口固定 30 秒,Standalone 固定 60 秒;轮询次数同时有上限。没有
`active`、Docker 命令失败或 container identity 漂移都按候选失败处理,不能由
Compose restart policy 掩盖。
### 4. 失败是 generation-forward recovery
若 attempted generation 有 previous generation,控制器使用 command 中预授权的
mutation/time调用既有 rollback CAS,创建 `attempted+1` generationimage 来自私有
immutable历史 revision。随后对新 generation 重新执行完整 preflight、Compose
apply和 active 验证。
若初始 generation 没有 previous,控制器显式 stop,结果为 `failed_stopped`。旧
digest rollback 也无法 active 时,不写成功/回退 receipt、不删除 rollout lock
由 operator 保留现场并原样重放。
返回语义:
- `active`:候选 generation 正常;
- `rolled_back`:坏候选已隔离,新的 rollback generation active
- `failed_stopped`:没有可恢复历史,service 已停止。
后两者 CLI 输出结构化结果但退出码为 2,避免自动化把已恢复的失败候选误报为发布
成功。
### 5. Receipt 是确定性私有事实
`service/rollouts/<rolloutId>.json` 为当前 UID、`0600`、no-replace 的 canonical
receipt,绑定 normalized command digest、attempted/final generation、结果和
active event digest。recorded time 取 command 已授权的 rollback time,因此
stage/rename/fsync 任一窗口重放都生成相同 bytes。
receipt 已存在时先严格复核完整 shape、canonical bytes、command digest 与结果内部
一致性,然后直接返回,不再次操作 Docker。receipt 已发布但 lock 未清理时,同一
命令只清理 exact lock。rollback selection 已推进而 receipt 缺失时,同一命令只
继续 rollback apply,不重新启动失败候选。
## 发布门
`ql3-local-compose-rollout-live-contract.cjs` 对 release pushed local digest 的 Edge
和 Standalone 分别创建 fresh authority、运行真实 compose-apply、验证私有
receipt,再发送 SIGTERM,要求应用发布 `event=stopped`
`stopResult=stopped`,最后 `compose down`。任一 Profile 失败会阻断共享 image
release workflow。
## 验收证据
- `local-owner-cli` 40/40
- image release contract 20/20
- 22 importer dependency/source audit 和 Edge import audit 零 finding
- workspace 保持 22 packages,无新增生产依赖;
- arm64 Docker Compose v5.3.1 上,Edge 128 MiB/64 PID 与 Standalone
256 MiB/256 PID 对 localhost exact manifest `sha256:e696…9b70` 均返回
rollout active、durable receipt、graceful cleanup
- 清理后没有遗留带 QingLong deployment generation label 的容器。
本机 localhost digest 只是 live evidence,不替代 GHCR 双架构 manifest、签名和
attestation 的正式 release 记录。
## 未包含
- migration 前 backup、写后 SQLite capability 与旧 image 的数据回退判定;
- Docker daemon/主机断电、ENOSPC 和 stage/fsync 实机 fault matrix
- systemd/OpenRC install、enable、健康与 rollback
- 正式 GHCR release 成功记录和物理路由器长期资源证据。