Commit Graph

6 Commits

Author SHA1 Message Date
fb22b7d49f docs(阶段5): 用生产快照跑完整切换演练
演练用 compose.debian.yml **本身**在本机 Docker 里跑,不是简化版。
数据是 2026-08-07 的 pg_dumpall 快照:1710 用户 / 956 题 / 123140 提交。

## 出口标准达成

停旧栈 11s,起新栈 34s(镜像预先构建好),全链路验证约 2 分钟 ——
**停机不到 1 分钟**,远在 30 分钟内。真正的时间风险在构建镜像(首次约 5 分钟),
所以手册里第一条就是「镜像必须在停机窗口之前构建好」。

验证到位的:首页、站点配置、题目列表、标签、公告、登录(argon2 新哈希和
Django pbkdf2 旧哈希都支持)、个人页、排行榜、后台四个接口、判题机自动注册,
以及**完整判题**(提交 Python A+B → AC,1.2 秒,两个测试点全过)。

## 两件原以为要做、实测不用做的事

- **不需要任何 DDL**:生产 dump 和新后端在用的库逐列对比,两边都是 278 列,
  零差异。新后端直接跑在现有结构上。
- **不需要重置序列**:我在 phase3-coverage.md 里记的那条「切换必做:重置序列」
  **是错的**,来自我手工按显式 id 导入、又没补 setval 的本地库。真实的
  pg_dumpall 带 30 条 setval,且把快照里所有序列和 max(id) 逐个对过,错位 0 个。
  已在原文档上标注更正,没有删掉原文 —— 错误结论本身也是信息。

## 回滚保证已实测

新栈跑完登录、提交、判题之后,再和生产 dump 比一次结构:逐列一致,零差异。
加上数据目录布局照抄旧后端,回滚 = 停新栈 + 起旧栈,约 20 秒,不动任何数据。
(未实测的部分也写明了:本机没构建旧 Django 镜像,「起旧栈」这一步没跑过。)

## 演练抓到的真问题

**pg_dumpall 备份会覆盖数据库口令。** 恢复完快照,新后端立刻报
`password authentication failed` —— 因为 dump 里带
`ALTER ROLE onlinejudge ... PASSWORD 'md5…'`,把角色口令覆盖成了备份时生产的那个。
正常切换不受影响(根本不恢复备份),但灾难恢复时这一条不写下来,
现场会被一个看起来毫不相干的报错卡住。

**恢复备份前必须先停应用**,否则 dump 里的 DROP DATABASE 失败。演练时因为
目标库是空的,数据照样进去了 —— 那是运气,目标库有数据就是满屏主键冲突。

## 镜像体积没达标,写明了原因

api 镜像 487MB,设计文档写的是「数十 MB」。一半以上(269MB)是 clang-format
拖进来的 LLVM,光 libLLVM.so 就 124MB。旧 Python 镜像同样装了 clang-format,
所以新镜像仍明显更小,但当初估「数十 MB」时没把它算进去。
瘦身路径也记了(换静态 clang-format 可砍 265MB),暂不做。

## 清理

演练在 data/postgres 留下了一份完整的生产数据副本,含 1710 名学生的
raw_password 明文列,已删除。手册里留了提醒 —— 那不是测试数据。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:13:59 -06:00
e3faa689e7 feat(阶段2补课): SQL 判题链路 + 最后两个后台端点
新后端此前完全没有 SQL 判题(旧 judge/sql_runner.py 378 行 + sql_dispatcher.py
113 行无对应实现),阶段 2 纵切时漏了这条与沙箱完全不同的路径。

  judge/sql/engine.ts   判题核心,移植自 sql_runner.py,判定口径逐条对齐
  judge/sql/child.ts    子进程入口
  judge/sql/index.ts    父进程:spawn + 硬超时
  judge/run.ts          language === "SQL" 时分流,不经判题沙箱
  POST admin/sql-test-cases/preview    题目页展示数据预览
  POST admin/sql-test-cases/generate   AI 按标准答案倒推初始化脚本

题目保存时重新生成 sqlDisplay(对齐旧 generate_sql_display):取测试点 1 的初始化
脚本 + 标准答案跑一遍,失败一律拦下不让保存 —— 展示数据直接决定学生看到的表结构
与期望结果,宁可不保存也不能存错的。

## 防护换了实现,逐条实测

bun:sqlite 没有 authorizer / progress_handler / setlimit,且实测 Worker.terminate()
杀不掉跑飞的查询(原生代码占着线程)。改用「WASM 引擎 + 独立子进程」:

  ATTACH  → WASM 无宿主文件系统绑定,结构上够不到(比旧的 authorizer 更强)
  查询题只读 → PRAGMA query_only=1
  超时    → 子进程外部 SIGKILL
  单值内存 → 子进程 ulimit -d

八条提交实测:正确→Accepted;列少一个/漏过滤→Wrong Answer;语法错误→Compile Error;
查询题里 INSERT→运行错误并说明;递归 CTE 死循环→CPU 超时;hex(zeroblob(2e8))→内存超限;
attach '/etc/passwd'→打不开。

## 踩到的两个坑(已写进 docs/specs/phase3-coverage.md)

1. ulimit 必须用 -d 不能用 -v。-v 限虚拟地址空间而 JS 引擎预留巨量地址,实测 -v 之下
   Bun 退出时有概率 panic(SIGILL),结果早已写出但进程异常终止,父进程读到空串
   误判成超时 —— 6 次里坏 2 次。换 -d 后 12/12 稳定。
2. 子进程写完结果直接 SIGKILL 自己,不走 process.exit()——后者仍有清理会撞限额。

至此 admin/api.ts 已无任何指向旧后端的调用。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:03:41 -06:00
764e0d28cc feat(阶段4): 标签管理 / 批量打标签 / 题目可见性 / 卡点与 AC 趋势 / 流程图 AI
GET/PUT/DELETE    admin/problem-tags[/:id]
  POST              admin/problems/batch-tag
  PUT               admin/problems/:id/visibility
  GET               admin/problems/stuck
  GET               admin/problems/ac-trend
  POST              admin/problems/flowchart

要点:

- 后台标签列表用 leftJoin 且不加 having,能看到 problemCount=0 的标签 ——
  那正是要清理的那些。oj 侧的 /problem-tags 才过滤 >0。
- 标签改名撞上已有标签视为合并:只给「还没挂目标标签」的题目补关系,
  否则会撞 (problem_id, problemtag_id) 唯一约束。
- 批量打标签:add 时按需新建标签、remove 时只认已有标签 ——
  否则「移除」会顺手造出一堆空标签。名字去重且大小写不敏感。
- **旧 ProblemVisibleAPI 的 `self.error(...)` 少写了 return**,题目不存在时会继续
  往下跑并抛 AttributeError(500)。这里正常返回 404。

顺带记下一条阶段 5 的必做项(见 phase3-coverage.md 文末):本地库按显式 id 从生产
导入,序列没跟着走,第一次新建标签就撞 problem_tag_pkey。只要切换流程里有
「导出→导入到新库」这一步,就必须重置全部序列,否则读全正常、第一次写才炸。

实测:学生 403;大小写重复与空白名去重后 tagCount=1、重复 add 幂等、
remove 不存在的标签 404 且不新建;纯改名 merged=false、合并 merged=true
affectedCount=1 且只剩一个标签;可见性取反两次复原、不存在的题 404。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:30:48 -06:00
359b91ab4d docs(阶段3): 记录 7 条 Minor 的逐条结论
其中两条判为无需改动并说明理由:M-2(user-progress 的 realName)已被 F2 的
sampleUser 默认关闭开关覆盖,现在与旧后端一样返回 null;M-3(练习答案下发)
旧后端逐字相同,属教程练习「客户端比对」的既有设计,不是本次重写引入的回归。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:01:13 -06:00
ced8f3ee1b docs(阶段3): 记录出口标准达成;补跑 prettier
覆盖率对账表更新:admin 侧 3/45 已实现,缺口 42。达成判据是 apps/web 的
oj/ 与 shared/ 两个目录已无指向旧 Django 的运行时调用,残留的 utils/http
引用只剩 ApiResponse 这一个类型。

顺带补跑 npm fmt,几个此前未格式化的文件随之改动,均为纯格式。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 06:48:20 -06:00
0f999aa5b1 docs: 阶段 0/1/2 出口标准核验 + 阶段 3 覆盖率对账
核验为逐条实跑,不采信文档声称。阶段 0/1/2 出口标准全部通过,
其中阶段 2 判题链路端到端实测(注册→登录→提交→JudgeServer→WS 推送)。

覆盖率对账:oj 侧 65 条已全部实现,admin 侧 45 条一条未做。
新旧路径不同名(API 已重新设计),对照关系为人工按语义逐条比对。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:25:36 -06:00