blob: 70b14d8bffe2fdc745e3d011570c4a311467ae81 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
# ETF Recovery Release Handoff — v4(2026-09-17)
状态:**READY_FOR_LEADER_REVIEW**(仅覆盖 ops 发布阻断 ASSET;应用代码自 v2 验收后已冻结)。
本版取代 v3;v3 中 leader 实际重跑 `ops-drill2.sh`(43+ PASS)与应用全部测试均已独立通过
(pytest 61 / cargo 58 / frontend 56,共 175 项)+ v3 演练 66 PASS 0 FAIL。
## 0. 仅剩一条真实发布阻断(前轮已修;本轮再加修两处执行路径缺陷)
前轮已修:旧 v2 场景 D 的 INJECT=rsync 在 backup 阶段就失败且不恢复旧服务(BD1–BD3 / D / G 断言,见第 2 节)。
本轮(v4)针对 leader 指出的两处执行路径缺陷,均在 `ops/deploy.sh` 内最小修复,脚本未重写:
- **A — restore 停服失败仍会写文件**。旧行为:`restore_release_id()` 中 stop 失败仅 note 后继续替换
runtime.env / release(运行中的二进制被覆盖)。修复(`deploy.sh:210-221`):stop 失败后必须再次
`is-active` 确认;**无法确认服务已停止即拒绝写任何文件**(含手动 restore 与自动回滚路径),文件
保持可恢复原状,die 明确报错退出非零;若 stop 失败但核实服务确已 inactive(常见于"早已停止"),
允许继续。
- **B — 停服后 DB 检查深层 exit 绕过恢复分支**。旧行为:`precheck_stopped()` 调用的
`check_no_inflight()` 在 DB 查询失败、DB 文件缺失或 runtime.env 缺 DB_PATH 时直接 `die`(深层
exit),主流程的 rc 捕获取不到,旧服务停在停机状态。修复(`deploy.sh:83-113` + 主流程
`deploy.sh:262-279`):`check_no_inflight` 改为**返回错误码而非 exit**(0=无任务 / 1=有任务 /
2=DB 或环境错误),`db_scalar` 失败不再 die;主流程把 rc=2 与竞态 rc=1 同样处理:**先恢复旧服务
(start),不替换任何文件**,_RB1/RB2 断言旧服务 active 且 env/binary/dist 无 swap_。
- 另修演练真实性:场景 D 的退出码断言此前形同虚设(输出非"0"的条件恒真),现真正捕获 deploy 退出码
(`rc_d=$?` 断言非零),不以管道/tail 掩盖。
## 1. 对象与固定 ID
- 数据集:国泰自由现金流 ETF 159399(market=cn,provider 手动修复数据集 id=`1f9e066a-e785-4ba5-bc9c-ef4742cd02d8`)。
- 候选 worker 镜像(**immutable ID,只按 ID 使用,禁止 retag**):
`strategy-lab-worker:etf-sina-candidate-r3` = `sha256:34a61c22c5135b6cc8453e5e3621674caf12284c5aa5dbfba1cda15299a656e3`
- 生产在线镜像:`strategy-lab-worker:local`(`bd9e9f06d56b…`,未动、未 retag)。
- 生产事实(只读核实):unit `strategy-lab-production`(UMask=0077)、环境由
`runtime.env`(BIND=127.0.0.1:8789 等)、发布目录 `/home/somhairle/.local/share/strategy-lab-production/release`。
## 2. v3 + 本轮(v4)相对 v2 的实际变更(全部仅在 `artifacts/etf-recovery-candidate/`)
### 2.1 `ops/deploy.sh`(共 329 行;v4 仅修改 A/B 两处,未重写)
| 位置(行号,v4 当前) | 变更 |
|---|---|
| `deploy.sh:51-53` | `fin_stage()`(manifest 先写 tmp 再原子 mv);注入钩子 `CP_CMD`/`MV_CMD`(默认 cp/mv,行为不变)。 |
| `deploy.sh:137-182` | `backup()`: 任一失败(env cp / release rsync / manifest 写 / finalize)都会退(备份目录改名 `<rid>.broken.<RID>`)后 `die`;写入 complete 校验(`runtime.env`、`release/strategy-lab-server`、manifest `previous_server_sha256`)全部成功才创建 `${BACKUP_DIR}/complete` 标记(0600)。 |
| `deploy.sh:260-273` | 主流程:**备份在 stop 之前进行**(phase 2/6)。备份失败 = 服务仍在运行,绝不转化为停机。 |
| `deploy.sh:274-303` | stop 后硬重检;rc=1(停后新任务)与 **rc=2(停后 DB/环境不可读)** 都走恢复分支:重启旧服务、什么都不替换、退出非零;备份目录仅供 inspection。 |
| `deploy.sh:83-116` | v4-B:`check_no_inflight()` 返回错误码不深层 exit(0 无任务 / 1 有任务 / 2 DB/env 错误);`db_scalar` 失败与缺 DB_PATH、DB 文件缺失一律 return 2。 |
| `deploy.sh:209-247` | `restore_release_id()`: complete/runtime.env/release 校验、deploy.lock;v4-A:stop 失败必须再次确认服务状态,**无法确认已停即拒绝写任何文件**(明确报错、退出非零、文件保持可恢复),确认 inactive 后继续。 |
| `deploy.sh:184-188` | `swap_release()`: `install ... || return 1; rsync -a --delete --checksum ... || return 1`(R1 根因修复已保留)。 |
| `deploy.sh:249-258` | `verify_restored()`: manifest 的 `previous_worker_image` 与 `previous_server_sha256` 逐一比对(损坏/伪造的 manifest 直接 `die`)。 |
| `deploy.sh:305-321` | swap/env(SED)/start-service/health 各自独立 rc-check,失败一律 `DEPLOY_LOCK_HELD=1 restore_release_id` 自动回滚(stop→替换→start→health 验证)。 |
### 2.2 `ops/rollback.sh`(共 103 行,重写)
- `rollback.sh:54`:无 `complete` 标记 → 拒绝恢复。`rollback.sh:45`:`list`命令显示 `COMPLETE`。
- `rollback.sh:62-66`:in-flight runs>0 → 拒绝(不当陷入)。
- `rollback.sh:72`:**先 `systemctl stop` 服务**,再 cp runtime.env / rsync release,替代任何在线文件被运行中进程写入。
- `rollback.sh:76-82`:文件替换失败时显式 `start`(尽力而为)并 FATAL exit 1。
- `rollback.sh:83-86`:启动后 `sleep 2` + is-active 失败 FATAL;`rollback.sh:88-90` health JSON `status=="ok"` 失败 FATAL;`rollback.sh:92-97` sha256 与 env(manifest `previous_worker_image`)不匹配 FATAL。
- 复审条目映射(与 deploy.sh 共享 lock `deploy.lock`,`rollback.sh:59-60`)。
### 2.3 `ops-drill2.sh` — 最终 **PASSED=84 FAILED=0 exit 0**(TDD:先加 RA/RB1/RB2 看其失败,修复后全绿)
- `fake-systemctl` 增加 `.audit.log`(stop/start/restart 顺序审计)。
- `FAKE_START_FAIL` 是一次性的(deploy 阶段 5 的首次 start 失败;restore/回滚路径的 start 不受影响)。
- `DEPLOY_PHASE=backup/swap/restore` 使注入只影响指定阶段。
- 既有场景(v3 均保留,全部仍通过):
- **BD1/BD2/BD3**(backup-cp / backup-rsync / backup-manifest 失败):断言 ① 旧服务 ACTIVE(零停机)② env/二进制/前端 sha256 逐字节不变 ③ 退出码非零 ④ 不允许存在带 complete 标记的备份。
- **D**(rsync 失败只在 swap 阶段一次性注入):备份先完整完成;自动回滚真实触发,断言服务 active、发布物/env 恢复逐字节一致、**真实捕获的非零退出码**。
- **G**(手动 rollback):audit 首行 == `stop`、末行 == `start`(先停服再写文件,服务最后启动),且最终服务 ACTIVE。
- 本轮新增场景:
- **RA**(restore 中 stop 失败且服务仍 active):断言明确拒绝信息、退出码非零、env/binary/dist 逐字节不变、服务仍 active(备份 intact 可恢复)。
- **RB1**(stop 后 DB 文件被删 → 查询错误):断言恢复旧服务 active、无任何 swap(env/binary/dist 不变)、退出码非零。
- **RB2**(stop 后 runtime.env 缺 DB_PATH):同上断言;runtime.env 其余键逐字节不变。
## 3. 最终可执行命令(仅指令,生产由 leader 执行)
```bash
# —— 只重跑合并后的 drill(隔离替身):
bash artifacts/etf-recovery-candidate/ops-drill2.sh # 期望:PASSED=84 FAILED=0 | exit 0
# —— 手动回滚(leader 生产会话执行):
artifacts/etf-recovery-candidate/ops/rollback.sh list # 只读,带 COMPLETE 标记
artifacts/etf-recovery-candidate/ops/rollback.sh <release_id> # stop -> 替换 -> start -> health/sha 验证
# (deploy.sh 自动回滚路径等价于 deploy.sh restore <release_id>)
```
部署命令(生产):
```bash
cd /home/somhairle/projects/strategy-lab/artifacts/etf-recovery-candidate/ops
IMAGE_ID=sha256:34a61c22c5135b6cc8453e5e3621674caf12284c5aa5dbfba1cda15299a656e3 \
STATE_DIR=/home/somhairle/.local/share/strategy-lab-production \
REPO_ROOT=/home/somhairle/projects/strategy-lab \
SERVICE=strategy-lab-production \
./deploy.sh deploy
```
(`deploy.sh` 默认值即上述 STATE_DIR/REPO_ROOT/SERVICE;显式列出仅供 review。IMAGE_ID 必须是唯一的不可变 ID。)
回滚(二选一):`./ops/rollback.sh <release_id>` 或 `./ops/deploy.sh restore <release_id>`;deploy 各失败路径会自动调用 restore。
## 4. 语义边界(去除过度承诺,本轮未变)
- **曾写 “重启后所有任务状态保持原样”——该过度承诺已删除**。真实语义:重启时正在执行的任务(dataset=running/pending、run=running/queued)不会被自动续上;重启可能把 running 置为 failed 或使其停留为 stale。恢复/重试由应用侧承担(DatasetCard 重试入口、server 调度),不属本 ops 脚本职责。
- 回滚目标 = pre-deploy 状态:`WORKER_IMAGE=strategy-lab-worker:local` + 旧二进制/旧 dist(manifest 记录 `previous_worker_image` 与 `previous_server_sha256`,恢复后强校验)。生产 `:local` 标签未被动过,回滚安全。
- **备份使用 0700 目录(runtime.env chmod 600)**;旧的备份目录(v2 期间产生的 `release-backups/20260917-*`),若 leader 未删除可能存在;脚本对现有备份目录不覆盖(同 RID 冲突 → 拒绝)。
- **有限保证**:`restore_release_id` 的 stop 失败拒绝路径保证"不写文件",但不保证 deploy 此前已换过的 env/binary 自动还原(该场景下文件保留原状 + 备份 intact,需 leader 手动 restore);停后 DB 错误分支只做到"恢复旧服务 + 不替换",DB 本身的损坏/丢失不在脚本的修复范围。
## 5. 遗留与上下文
- **应用冻结**:pytest/cargo/frontend 测试全部通过(pytest 61 / cargo 58 / frontend 56),leader 独立重跑通过,共 175 项(61+58+56)。qa2(隔离替身实测)之后未再改动应用代码。
- **qa2 独立验收未变**:`artifacts/etf-recovery-candidate/qa2/`(登录、数据集 ds_cn/ds_sz ready、预览/回测 succeeded `b4339ff5-`、6 步浏览器重试通过)。
- 禁止 export/输出真实密钥/生产路径;禁止一切生产改动(本会话从未执行生产 systemctl/docker/rsync)。
- 复审条目映射:R1 = swap/自动回滚路径独立 rc-check(deploy.sh:184-188 与 305-321);R2 = 停后硬重检 + 恢复分支(deploy.sh:274-303,v4 扩展 rc=2);R3 = restore 锁+health+hash(deploy.sh:209-258 / rollback.sh 锁+health+hash);R4 = drill 注入矩阵 + exit 语义(FAILED>0 → exit 1,v4 加 RA/RB1/RB2)。
- leader 审查入口:重跑 `ops-drill2.sh`(命令见第 3 节);生产动作一律由 leader 执行;改动仅涉及 `ops/deploy.sh`、`ops-drill2.sh` 与本文档,未触应用代码,未提交任何 git commit。
|