feat(ql3): rehearse legacy sqlite upgrade

This commit is contained in:
whyour
2026-08-21 00:52:58 +08:00
parent 4ea156f189
commit c9e41812cb
23 changed files with 1533 additions and 23 deletions
+8 -2
View File
@@ -157,6 +157,11 @@ receipt。
### 2.2 adopted 2.x 的 Docker legacy silence gate
已有 2.x SQLite 部署必须先完成
[`inspect → stage → verify → activation`](./ql3-local-sqlite-adoption.md),取得真实、未手写的 manifest、activation 与 digest。
下列 silence/cutover 命令只消费该证据,不负责隐式迁移数据库。source、target 和 recovery 必须一直保留到回退或 reconciliation
正式闭合。
adopted target 不得直接使用旧 v1 配置启动。当前第一条可达的 cutover 产品路径只接管由
Docker 精确容器 ID 标识的 2.x ownersystemd/OpenRC legacy、Kubernetes 和远端 cluster
不允许伪装成 Docker evidence。先确保 deployment root 与其 `service/` 都是当前 UID 的
@@ -399,11 +404,12 @@ reconciliation_required
manual_review
```
- `rollback_candidate`target 主文件仍等于 activation 初始 SHA-256、source 仍等于 recovery SHA-256
- `rollback_candidate`target 主文件仍等于 activation `targetSha256`、source 仍等于 activation 的
`sourceSha256`
且两者均没有 `-wal``-shm``-journal` sidecar。
- `reconciliation_required`:target 主文件已经变化,或发现 target SQLite sidecar;必须保留 target 并进入
后续数据域 reconciliation,不能启动 2.x。
- `manual_review`activation/文件稳定身份不能证明,或 target 未写但 source 已偏离 recovery;不得猜测。
- `manual_review`activation/文件稳定身份不能证明,或 target 未写但 source 已偏离 activation 时原始 source;不得猜测。
`rollback_candidate` 本身不是 legacy restart 授权。实例 head 成功后进入 `target_stopped`,旧 restart
command 会在 Docker authority 前失败;只有下一节的双阶段 ceremony 可以请求启动 2.x。
@@ -0,0 +1,143 @@
# QingLong 2.x SQLite 接管到 3.0
本流程把已有 QingLong 2.x `database.sqlite` 以不覆盖 source 的方式接管为 3.0 adopted storage。它只处理单个 SQLite 主库;
scripts、configs、logs 和其他 data directory 文件不在本流程内。
全部命令由最终运行 QingLong 的同一个 POSIX 用户执行。`ql3-adoption` 是一次性 CLI,不启动服务,也不会自动执行 cutover 或
Legacy rollback。
## 1. 空间与停机前检查
- `deploymentRoot` 及输出父目录必须是当前 UID、canonical、非 symlink 的 `0700` 目录;
- command file 必须是当前 UID、canonical、单链接 `0600` 文件;
- source 必须是当前 UID 的 canonical 普通单链接文件,且 group/world 不可写;生产环境建议 `0600`
- target、recovery、manifest、activation 必须尚不存在,并全部位于 `deploymentRoot` 内;
- source、target、recovery、manifest、activation 五个路径必须互不相同;
- 预留 recovery + target 两份数据库以及 SQLite 临时/sidecar 余量。不要以删除 recovery 的方式解决空间不足。
inspect 和 Online Backup 可以在 2.x 仍可读时执行,但最终 stage review 后到 activation/cutover 期间必须停止外部 writer,并保证
source 不再漂移。activation prepare 会尝试取得 source/target write fence;无法取得时失败关闭。
低配设备说明:四个阶段都是 one-shot 进程,没有后台 timer、监听端口或自动重试;schema inventory 最多 4096 项,manifest 最多
256 KiBSQLite backup 按页复制而不是把整个数据库载入 JS heap。制品审计的 import RSS 不是迁移峰值承诺,正式升级前仍需在目标
路由器/NAS 上测量可用空间、耗时、峰值 RSS 和断电恢复。
## 2. Inspect:生成待审核计划
```json
{
"schemaVersion": 1,
"operation": "local-sqlite.adoption.inspect",
"options": {
"deploymentRoot": "/opt/qinglong3",
"profile": "edge",
"sourcePath": "/opt/qinglong/data/database.sqlite",
"legacyTimezone": "Asia/Shanghai"
}
}
```
```sh
chmod 0600 /secure/operator/ql3-sqlite-inspect.json
ql3-adoption run --command-file /secure/operator/ql3-sqlite-inspect.json
```
记录返回的 `planDigest`,审核 schema catalog、table names 与 task inventory。不要在审核后修改 source;若 source 变化,重新 inspect
不要沿用旧 digest。
## 3. Stage:生成 recovery、target 与 manifest
```json
{
"schemaVersion": 1,
"operation": "local-sqlite.adoption.stage",
"options": {
"deploymentRoot": "/opt/qinglong3",
"profile": "edge",
"sourcePath": "/opt/qinglong/data/database.sqlite",
"targetPath": "/opt/qinglong3/artifacts/qinglong3.sqlite",
"recoveryPath": "/opt/qinglong3/artifacts/database.pre-ql3.sqlite",
"manifestPath": "/opt/qinglong3/artifacts/qinglong3-adoption.json",
"expectedPlanDigest": "REPLACE_WITH_INSPECT_PLAN_DIGEST",
"legacyTimezone": "Asia/Shanghai"
}
}
```
```sh
chmod 0600 /secure/operator/ql3-sqlite-stage.json
ql3-adoption run --command-file /secure/operator/ql3-sqlite-stage.json
```
stage 通过 SQLite Online Backup 创建独立 recovery 和 target,只在 target 上执行 3.0 migration,并以 `0600` no-replace 文件发布
结果。source 不会被覆盖。任一输出已存在时不要删除后盲目重跑,应先把现有文件和命令结果作为一次未完成 ceremony 调查。
## 4. Verify:独立复验 staged 结果
```json
{
"schemaVersion": 1,
"operation": "local-sqlite.adoption.verify",
"options": {
"deploymentRoot": "/opt/qinglong3",
"profile": "edge",
"targetPath": "/opt/qinglong3/artifacts/qinglong3.sqlite",
"recoveryPath": "/opt/qinglong3/artifacts/database.pre-ql3.sqlite",
"manifestPath": "/opt/qinglong3/artifacts/qinglong3-adoption.json"
}
}
```
```sh
chmod 0600 /secure/operator/ql3-sqlite-verify.json
ql3-adoption run --command-file /secure/operator/ql3-sqlite-verify.json
```
记录返回的 `manifestDigest`。verify 只接受与 manifest 一致的 recovery/target、完整 migration 和通过的 adopted readiness。
## 5. Prepare activation:冻结三份物理事实
```json
{
"schemaVersion": 1,
"operation": "local-sqlite.activation.prepare",
"options": {
"deploymentRoot": "/opt/qinglong3",
"profile": "edge",
"sourcePath": "/opt/qinglong/data/database.sqlite",
"targetPath": "/opt/qinglong3/artifacts/qinglong3.sqlite",
"recoveryPath": "/opt/qinglong3/artifacts/database.pre-ql3.sqlite",
"manifestPath": "/opt/qinglong3/artifacts/qinglong3-adoption.json",
"activationPath": "/opt/qinglong3/artifacts/qinglong3-activation.json",
"expectedManifestDigest": "REPLACE_WITH_VERIFY_MANIFEST_DIGEST"
}
}
```
```sh
chmod 0600 /secure/operator/ql3-sqlite-activation.json
ql3-adoption run --command-file /secure/operator/ql3-sqlite-activation.json
```
activation 分别记录 `sourceSha256``recoverySha256``targetSha256`。SQLite Online Backup 的 recovery 与 source 可以逻辑等价
但物理字节不同,因此不能比较两者哈希来判断 clean rollback。记录返回的 `activationDigest`,并把 exact activation path/digest
交给 adopted application 和后续 cutover 命令。
## 6. Cutover 与回退判定
完成 activation 后,继续执行 [Edge/Standalone 部署准备](./ql3-local-deployment.md) 中的 legacy silence、target start/stop 和双阶段
rollback ceremony。不要仅凭 activation 启动两个 writer。
target stop 后的数据分类为:
- `rollback_candidate`target/source 都保持 activation 时字节且双方 sidecar clear
- `reconciliation_required`:target 已接受写入或存在 target sidecar;保留三份数据库,禁止自动启动 2.x;
- `manual_review`source 漂移、activation 或稳定文件身份无法证明;禁止猜测。
证据字段 `sourceMatchesActivation` 表示 source 与 activation 时的原 source 哈希相同,不表示 source 与 Online Backup recovery 文件
物理相同。`rollback_candidate` 也不是启动授权,仍须走双阶段 Legacy rollback。
## 7. 当前边界
本流程不迁移完整 2.x data directory,不做 target 写后的自动逆迁移,不连接 Cluster 控制面,也没有云端恢复服务。任何
`reconciliation_required` 必须保留现场并等待后续显式数据域工具;不要把 target 覆盖回 source。