fix(server,test): W6-A stabilize CI — sync lost update, atomic recovery summary, serial PG tests

- downloads.ts observe: re-read download_task FOR UPDATE inside the tx and merge
  from the locked row; qB sync could overwrite imported back to importing when
  verifyAndImport committed between the sync read and write (E8 flake).
- agent finalize: accept summaryPrefix so the recovery verdict is written with
  the terminal status in one update (E6 read a budget_exceeded run before the
  "未恢复:…" annotation landed); recover-summary step kept, now idempotent no-op.
- organize.ts: drop local slotComplete and the once-subscription completion
  write; completion/history are decided only by applySlotEvent(verify_passed)
  (W5-C follow-up, removes double write). Manual slot organize uses the shared
  subscriptions.slotComplete.
- vitest: server split into unit (parallel) and pg (file-serial, groupOrder 1)
  projects; packages/tasks file-serial; explicit worker concurrency in e2e.
- docs: W6 stabilization record in 04-v0.1-acceptance.md.

Test expectations unchanged. Full `deno task ci` x3 green: 369s / 341s / 337s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 00:32:57 +08:00
1 parent 36aa99b8aa
commit 887843f777
10 files changed
+198 -143

No files matched your search

+33
View File
@@ -214,3 +214,36 @@
3. **按 03 清单把 IO 与 Agent 分成两个 worker 组**,给 verifyAndImport、runAgent 分配独立并发度,并加事件循环延迟探针。在性能基线(REQ §16.2)里记录文件数、挂载类型和 ci 耗时,防止 ci 从约 135 秒继续膨胀(可以考虑按包并行跑 `-j`)。
4. **e2e 矩阵扩到剧集和部分失败**:剧集追更(S5)、整季包部分已有(AT54)、部分文件入库后不清理(AT29)、多下载器同一 hash(AT73)。这些是 M2 综艺分段建模的前置样本。
5. **把恢复和版本升级纳入 CI**:用 docker compose 跑"备份 → 清库 → 恢复 → 只读对账 → 映射比对"的夜间任务,再加一条 openworkflow 版本升级的回归测试,避免只靠人工演练记录。
## W6 治稳记录(2026-09-28)
起点:W4/W5 全部合入后两次全量 `deno task ci` 各挂不同用例(run1 E8 两例:`download_task` 停在 `importing`、C 没被清理;run2 E1 等 qB 收种超时、E6 预算超限用例汇总文案不符)。
**区分方法**:10 个 e2e 文件逐个串行各跑 3 次,30/30 全绿;失败只在 44 个 server 测试文件并发全量时出现。结论是没有"稳定必现"的回归,但其中两处是并发下真实存在的产品竞态,另一处是负载导致的超时。
### 真实缺陷(并发暴露)与修法
| 现象 | 根因 | 修法 |
|---|---|---|
| E8:入库记录已 succeeded,`download_task` 仍是 `importing`;随后缓存清理不认它(期望 removed 实得 importing) | qB 同步 `observe` 先在事务外读 `download_task` 快照,再整行覆写 `status`;入库事务在两者之间把 `importing→imported` 提交后,同步按旧快照合并出 `importing` 写回(丢失更新)。负载高时窗口变大 | `services/downloads.ts observe`:事务内 `select … for update` 重读该行,按锁住的最新状态合并后写回(`mergeObservedDownloadState` 对 imported 保持不变) |
| E6 预算超限:run 已是 `budget_exceeded`,summary 还是 finalize 的原文,之后才被改写成"未恢复:调查超出预算…" | 授权内恢复的核实结论由 finalize 之后单独一步 `annotateRunSummary` 追加,终态与最终汇总分两次写,观察者(UI/SSE/测试)能读到中间态 | `finalize` 新增 `summaryPrefix`,runAgent 把恢复结论随终态同一次写入并推送;`recover-summary` 步骤保留(已带前缀时幂等跳过),兼容旧 run 的步骤历史 |
| (W5-C 遗留双写) | `organize.ts verifyAndImport` 仍用本地 `slotComplete` 判定并直接把 once 订阅置 completed,与 `applySlotEvent(verify_passed)` 按总集数账本的判定并存;总集数未知/订阅 pending 时两者会给出相反结论 | 删除 organize 本地 `slotComplete` 与订阅完成更新,完成与历史只由 `applySlotEvent` 钩子负责;手动整理指定 Slot 的分支改调 `services/subscriptions.ts slotComplete` |
测试期望未改。
### 不稳定项与治法
- E1 等 qB 收种 60s 超时:全量并发时 44 个文件各起真实 worker/outbox/后台循环抢 CPU 与连接,编排链被拉长。治法:`apps/server/vitest.config.ts` 分两个 project,`unit`(明确列出的纯内存文件)并行,`pg`(其余全部,含 e2e 与 PG 集成,新文件缺省归此)`fileParallelism:false` 且 `groupOrder:1` 排在 unit 之后;`packages/tasks` 的 PG 引擎测试同样文件级串行。
- worker 并发在 e2e 夹具与崩溃恢复子进程里显式配置(io 4 / agent 2),不受环境变量影响;钩子超时 120s、teardown 60s。
- 已有保障复核:每文件独立业务 schema + 引擎 schema(pid+随机后缀),建/删在咨询锁上串行;mock qB/TMDB/RSS/站点端口均为 0(随机);等待均走 `waitFor` 轮询 + 超时。E8/E9 里剩下的 1.5s 固定等待是"一段时间内不应发生"的反向断言,保留。
- 未引入任何重试。
### 3 次全量 ci
| 次 | 结果 | 耗时 | 其中 server vitest |
|---|---|---|---|
| 1 | 全绿(server 44 文件 / 259 用例) | 369s | 227s |
| 2 | 全绿 | 341s | 232s |
| 3 | 全绿 | 337s | 228s |
代价:server 测试从并发约 90s 变为串行约 230s,tasks 约 52s;换来的是时序预算可预期。后续如需提速,可按 e2e 与集成拆成两个串行组分进程跑。