mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
126 lines
6.4 KiB
Markdown
126 lines
6.4 KiB
Markdown
# ADR-0202:显式、可重放的 Compose 恢复证据收集
|
||
|
||
- 状态:Accepted
|
||
- 日期:2026-07-29
|
||
- 关联 RFC:QL-RFC-0001 D-175、D-190、D-191、D-192
|
||
- 关联 ADR:ADR-0185、ADR-0200、ADR-0201
|
||
|
||
## 背景
|
||
|
||
ADR-0200/0201 将 rollout backup 与 restore safeguard 作为不可变恢复证据保留,
|
||
并分别设置 8/4 份目录上限。该策略避免了自动恢复和无界增长,但如果没有受审的
|
||
空间回收入口,低闪存路由器最终只能停止升级或恢复。
|
||
|
||
直接删除 `.sqlite` 也不安全。旧 `compose-apply` 和
|
||
`compose-restore-commit` 的精确回放会重新检查原快照;文件消失后,调用方无法
|
||
区分“已经受审收集”与“意外丢失”,甚至可能把响应丢失误判为需要再次启动或恢复。
|
||
收集过程还可能在 rename、tombstone、commit receipt 或 unlink 之间崩溃。
|
||
|
||
## 决策
|
||
|
||
### 1. 收集是显式、短生命周期的两阶段操作
|
||
|
||
既有 `ql3-local-deploy` 增加:
|
||
|
||
- `compose-evidence-collect-prepare`
|
||
- `compose-evidence-collect-commit`
|
||
|
||
命令只接受当前 UID 私有的 exact command file,不接受通配符、目录、任意路径、
|
||
自动年龄阈值或调用方提供的 snapshot digest。prepare 显式列出 rollout/restore
|
||
UUID,只验证候选、当前 generation、Profile、保留底线和 immutable receipt/snapshot
|
||
绑定,并发布 collection lock 与 prepare receipt;它不删除或移动任何大文件。
|
||
|
||
commit 必须使用同一 collection ID/generation,且时间不早于 prepare。
|
||
|
||
### 2. Profile 固定保留底线和单次预算
|
||
|
||
- Edge:每次最多收集 1 个文件;至少保留 2 个 rollout backup、1 个 restore
|
||
safeguard。
|
||
- Standalone:每次最多收集 4 个文件;至少保留 4 个 rollout backup、2 个
|
||
restore safeguard。
|
||
|
||
每一类只能选择按 durable receipt `recordedAtMs + UUID` 排序后的最老前缀,不能
|
||
跳过旧证据先删较新的恢复点。某一类没有收集候选时,不要求部署人为制造该类最低
|
||
数量。restore safeguard 只有存在 terminal commit receipt 时才是候选。
|
||
|
||
这些底线不是自动 retention policy。系统没有 timer、watcher 或磁盘水位触发器;
|
||
达到 8/4 上限时仍由 operator 明确审查并执行。
|
||
|
||
### 3. Tombstone 保持旧命令的精确回放语义
|
||
|
||
每个已收集大文件对应一个小型、append-only、`0600` tombstone,绑定:
|
||
|
||
- artifact kind 与 UUID;
|
||
- collection ID、generation、Profile 和 commit 时间;
|
||
- 原 rollout/restore terminal receipt 的 SHA-256;
|
||
- SQLite contract、SHA-256、bytes、page count 和 page size。
|
||
|
||
旧 apply/restore 回放在大文件存在时继续检查物理 snapshot;文件不存在时只接受与
|
||
当前不可变 source receipt 和原 snapshot facts 全部匹配的 tombstone。否则失败
|
||
关闭。restore 回放明确返回 `source|safeguard=collected`,并保持
|
||
`service.state=unchanged`;collection 不获得 Docker authority。
|
||
|
||
Tombstone 和 prepare/commit receipt 体积有硬上限,但当前版本不自动压缩或删除
|
||
tombstone 历史。它解决大 SQLite 文件的闪存占用,不宣称元数据永远有界;后续若
|
||
需要压缩,必须有独立的摘要归档协议。
|
||
|
||
### 4. Commit 使用 rename → tombstone → receipt → unlink
|
||
|
||
每个 snapshot 在原目录被 rename 到确定性的
|
||
`.ql3-collection-stage`,复验完整 SQLite evidence 后发布 tombstone。全部
|
||
tombstone durable 后发布 collection commit receipt,最后 unlink stage 并删除
|
||
collection lock。
|
||
|
||
原命令重放必须覆盖:
|
||
|
||
- final 已 rename、tombstone 尚未发布;
|
||
- tombstone 已发布、commit receipt 尚未发布;
|
||
- commit receipt 已发布、stage 或 lock 尚未清理;
|
||
- response 已丢失但 durable commit 已完成。
|
||
|
||
收集前后都验证 source receipt digest。final/stage 冲突、receipt/tombstone 漂移、
|
||
generation 变化、权限异常或未完成的其他 operation 都失败关闭。
|
||
|
||
### 5. 与 revision、rollout、restore 双向围栏
|
||
|
||
collection prepare 在发布自己的 lock 前后检查 revision、rollout 和 restore lock。
|
||
revision/apply/restore 在 mutation 前检查 collection lock,并在发布自身 lock 后
|
||
再次检查。竞争中只能有一方保留 durable authority;失败方在任何 image、Docker
|
||
或 SQLite mutation 前释放自己的新 lock。
|
||
|
||
### 6. 不新增 package 和常驻成本
|
||
|
||
实现位于现有 `@qinglong/local-owner-cli/local-deployment` 的 package 内部目录,
|
||
复用 `@qinglong/local-sqlite/rollout-safety` 检查 snapshot。没有新增 workspace
|
||
package、第三方生产依赖、daemon、端口、listener、timer 或 watcher。该选择也落实
|
||
ADR-0185:小能力优先用包内 subpath 表达,只有独立交付/权限/依赖边界才拆 package。
|
||
|
||
## 验收证据
|
||
|
||
- `local-owner-cli` 48/48,通过现有部署、rollout、restore 与新增 collection
|
||
回归;
|
||
- 专项覆盖 Edge 最老前缀、保留底线、rename 后崩溃恢复、commit 精确重放、
|
||
collection/revision 围栏;
|
||
- rollout backup 收集后,旧 apply 只读 tombstone 并且不产生新的 Compose up;
|
||
- restore safeguard 收集后,旧 restore commit 返回
|
||
`safeguard=collected, service.state=unchanged`;
|
||
- TypeScript build 通过,workspace 仍为 22 个 package且没有新增生产依赖。
|
||
|
||
真实断电、闪存 wear/写放大和长周期 tombstone 压缩仍是独立门禁。
|
||
|
||
> 2026-08-01 补充:SQLite write contract 已推进到 v42。Tombstone v1 的 snapshot
|
||
> contract 不能固定为创建本 ADR 时的 v40,否则当前证据不可收集;也不能只改成 v42,
|
||
> 否则升级前的 v40/v41 证据无法回放。当前 reader 接受受审历史窗 `40..42`,最大值由
|
||
> 已经合法持有 `local-sqlite/rollout-safety` authority 的 apply/collection/restore 调用方
|
||
> 显式传入;evidence 模块自身不新增 storage import,未来版本仍默认拒绝。v42 部署专项
|
||
> 23/23、local image audit 7/7、19-package 全量门和 dependency boundary 均通过。
|
||
|
||
## 被拒绝的替代方案
|
||
|
||
- **按 mtime 自动删除**:mtime 不是恢复 authority,且会引入后台资源和竞态。
|
||
- **删除 receipt 与 snapshot**:破坏精确回放和审计链。
|
||
- **只留下“已删除”布尔值**:不能绑定原文件内容和 source receipt。
|
||
- **先 unlink 再写 tombstone**:进程崩溃会把受审收集变成不可区分的数据丢失。
|
||
- **另建 backup/GC package 或 daemon**:没有独立交付边界,并增加低配节点安装和
|
||
idle 成本。
|