feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,132 @@
# ADR-0201:显式、围栏化的本机 SQLite 恢复
- 状态:Accepted
- 日期:2026-07-29
- 关联 RFCQL-RFC-0001 D-184、D-189、D-190、D-191
- 关联 ADRADR-0194、ADR-0199、ADR-0200
## 背景
ADR-0200 在 generation 2+ rollout 前保存在线一致的 SQLite snapshot,并明确禁止
健康失败后自动覆盖数据库。这样避免了候选已经提交有效事实时的静默丢失,但当候选
和旧 image 都无法安全运行时,operator 仍需要一个可执行、可审计、可崩溃重放的
data restore。
Docker selection、rollout lock、SQLite 文件和 receipt 之间没有跨介质事务。恢复
过程还必须覆盖磁盘满、服务未真正停止、prepare 后数据库漂移、进程在 rename
之间退出,以及 commit receipt 已发布但锁尚未清理等窗口。低配路由设备不能为此
常驻另一个恢复 daemon。
## 决策
### 1. 恢复只处理失败 rollout 的既有证据
恢复入口复用 `ql3-local-deploy`,只接受私有 exact-shape command file,并拆成:
- `local.deployment.compose.restore.prepare`
- `local.deployment.compose.restore.commit`
prepare 必须绑定仍存在的原始 `.compose-rollout.lock`、该 rollout 的不可变
snapshot、已经前进到 rollback generation 的 active selection,以及同一 Docker
executable/socket。存在成功 rollout receipt、generation 漂移、不同 rollout ID
或 source snapshot 漂移时一律失败。
该入口不是任意 SQLite 文件复制工具,也不接受调用方提供 source path 或 digest。
### 2. Prepare 只建立恢复授权,不覆盖当前数据库
prepare 先发布 exact `.compose-restore.lock`,再显式停止 Compose service,并通过
container inspect 证明它不在运行。随后:
1. 对当前数据库执行 `wal_checkpoint(TRUNCATE)`
2. 切换到 `journal_mode=DELETE`,拒绝残留 `-wal`/`-shm` sidecar
3. 记录当前数据库完整 contract/page/bytes/SHA-256
4. 创建当前数据库的不可变 safeguard;
5. 发布 command-digest-bound prepare receipt。
source、current 和 safeguard 都必须为当前 UID `0600`、单链接、完整且
`integrity_check=ok` 的 self-contained SQLite 文件。source 与 current SHA 必须
不同。prepare 结束时服务保持停止,数据库内容不被覆盖。
每个 restore ID 对应一个 safeguard,目录最多保留 4 份。达到上限时新 prepare
失败关闭;本决策不自动删除恢复证据。
### 3. Commit 以 current SHA 作为最后一道数据围栏
commit 只接受与 prepare lock/receipt 相同的 deployment root、Docker
executable/socket、restore ID 和 generation,且 commit 时间不得早于 prepare。
它再次停止并检查 service,重新 checkpoint 当前数据库,并要求 SHA 仍等于
prepare 记录的 current,或者已经等于 source(commit 响应丢失的重放状态)。
prepare 后任何新提交都会改变 SHA,使 commit 在覆盖前失败。operator 必须保留
现场并重新决策,不能通过改 command 或 receipt 绕过。
### 4. 文件替换是可恢复状态机
commit 使用同一文件系统上的确定路径执行:
```text
source snapshot -> restore stage
current database -> restores/<restoreId>.replaced.sqlite
restore stage -> current database
```
每一步都复验 SQLite evidence 并同步 file/directory。崩溃后重放可从以下状态收敛:
- stage 已创建、current 尚未移动;
- current 已移动、数据库路径暂时缺失;
- source 已安装、replaced 尚未清理;
- database 已恢复、commit receipt 尚未发布;
- receipt 已发布、restore lock 尚未删除。
ENOSPC 创建 stage 时会清理不完整 stage,当前数据库保持原位;replaced evidence
缺失或 SHA 不符时不猜测恢复。
### 5. 恢复数据后仍由原 rollout 完成服务恢复
commit 发布不可变 receipt 后删除 `.compose-restore.lock`,但故意保留原始
`.compose-rollout.lock`。此时 SQLite 已恢复,service 仍停止。operator 必须原样
重放原来的 `compose-apply` command,让旧 image 重新通过当前数据库的完整
read/write contract preflight、启动并发布最终 rollout receipt。
因此 data restore 不伪装成 service recovery。恢复锁存在期间 `compose-apply`
直接失败关闭;commit receipt 的只读重放不会再次停止一个后来已经恢复运行的
service,结果明确返回 `service.state=unchanged`
### 6. 保持低配设备闭包
实现继续位于既有 `@qinglong/local-sqlite/rollout-safety`
`@qinglong/local-owner-cli/local-deployment`,不新增 workspace package、第三方
依赖、进程、端口、listener、timer、watcher 或自动 GC。只有 operator 显式执行
短生命周期命令时才加载恢复代码;systemd/OpenRC 与 Cluster PostgreSQL 不使用
该文件协议。
## 验收证据
- `local-sqlite` 144/144storage primitive 覆盖 Edge/Standalone checkpoint、原子 restore、
current-moved 崩溃恢复与 ENOSPC
- `local-owner-cli` 46/46;覆盖 prepare/commit、restore lock 阻断 rollout、post-prepare SHA
漂移、safeguard 容量失败、current-moved 恢复、原 rollout 继续完成及低敏回放;
- 类型检查与 22-package 依赖边界审计通过,无新增生产依赖;
- arm64 Docker 上,Edge 128 MiB/64 PID 与 Standalone 256 MiB/256 PID 对同一
localhost exact manifest 都完成 generation `1→2`、generation 3 写入后健康证据
失败、rollback generation 4、prepare/commit、原 rollout 恢复、commit 回放不
停止服务及 graceful cleanup;临时 Compose 资源为零;
- PostgreSQL 18.4 arm64 physical HA 回归门使用本地 exact repo digest,完成
`remote_apply`、timeline `1→2`、旧主 fencing、`pg_rewind` 同步只读重加入,
全部领域门 `gates.passed=true`,且无残留 HA Docker 资源。
真实 Compose data restore、物理断电、不同文件系统、闪存寿命与恢复点 GC 仍是
独立后续门禁;本 ADR 不把单元故障注入冒充物理设备证据。
## 被拒绝的替代方案
- **候选失败自动恢复 snapshot**:会丢弃候选已提交的有效事实。
- **单条 restore 命令直接覆盖**:没有 operator 审查窗口,也无法围栏 prepare 后
新写入。
- **先删除 current 再 copy source**ENOSPC 或断电会失去唯一当前数据库。
- **接受任意 source path**:把部署恢复扩大为宿主机文件读取/覆盖 authority。
- **commit 顺便启动容器**:把 data restore 与 image compatibility、健康恢复再次
混成不可原子提交的操作。
- **后台自动清理 snapshot/safeguard**:增加路由器 idle 成本,并可能删除尚未
完成人工审查的恢复证据。