mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
129 lines
5.8 KiB
Markdown
129 lines
5.8 KiB
Markdown
# ADR-0199:Generation-Fenced 本机 Compose Rollout
|
||
|
||
- 状态:Accepted
|
||
- 日期:2026-07-29
|
||
- 关联 RFC:QL-RFC-0001 D-184、D-187、D-188、D-189
|
||
- 关联 ADR:ADR-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` generation,image 来自私有
|
||
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 成功记录和物理路由器长期资源证据。
|