10 KiB
R01-D4-2 claim token 最终独立复验报告(冻结前最后一次)
日期:2026-09-16
分支:refactor/r01-structure-performance
HEAD:b10c4a732740e42353ca826f63141c3dd2a49915(复验全程未变)
复验对象:docs/r01-d4-2-claim-fence-remediation-20260916.md 的 claim token 整改
性质:仅复验 —— 未修改生产代码、未修改既有测试、未暂存、未提交、未推送
0. 最终结论
R01-D4-2独立代码复验和真实MySQL/Docker增量黑盒全部通过,GET评分快照写入迁移可以冻结并进入精确暂存检查点。
FAIL = 0,SKIP = 0。 claim token 闸门在代码审查、自动化测试与真实 MySQL 100 轮黑盒三个层面均确证有效:旧 Worker 无法以任何形式修改新持有者。
1. 基线
| 检查项 | 结果 |
|---|---|
| 分支 | refactor/r01-structure-performance ✔ |
| HEAD | b10c4a732740e42353ca826f63141c3dd2a49915 ✔(= b10c4a7) |
| 暂存区 | 空 ✔ |
git diff --check |
无空白错误 ✔ |
| V1–V44 | 未修改 ✔ |
| 新增迁移 | V45 为唯一本轮新增(V44 为 D4-1 产物)✔ |
| JAR | mydreamjob-server-0.0.1-SNAPSHOT.jar,2026-09-16 17:26:51(最新 clean verify 产出)✔ |
2. 独立代码审查
CandidateScoringRefreshTaskExecutor.java(153 行,逐行核对):
| 核查项 | 结论 | 证据 |
|---|---|---|
| token 在领取 SQL 中原子生成并写入 | ✔ | :53 UUID.randomUUID().toString();:56 SET status='PROCESSING', processing_started_at=?, claim_token=?, ... 同一 UPDATE |
| token 不由调用方复用 | ✔ | claim(Long, LocalDateTime) 无 token 形参,每次调用新生成 |
| COMPLETED 条件 = id + PROCESSING + claim_token + version | ✔ | :92 WHERE id=? AND status='PROCESSING' AND claim_token=? AND version=? |
| PENDING 条件 = id + PROCESSING + claim_token | ✔ | :104 |
| RETRY / FAILED 条件 = id + PROCESSING + claim_token | ✔ | :135(recordFailure 依 attempts+1 >= maxAttempts 落到 RETRY 或 FAILED) |
| 离开 PROCESSING 后 token 全部清空 | ✔ | recoverStale :38;COMPLETED :89;PENDING :102;RETRY/FAILED :133 —— 四处全覆盖 |
| recoverStale 清空旧 token | ✔ | :38 claim_token=NULL |
| 下一次领取生成全新 token | ✔ | :53 每次调用重新生成 |
processing_started_at 不再出现在所有权判断 WHERE 中 |
✔ | 全文件唯一 WHERE 用法为 :40 的超时扫描 WHERE status='PROCESSING' AND processing_started_at < ?;所有权判定全部使用 claim_token |
| 更新 0 行按 stale claim 处理 | ✔ | claim :61-63 返回 null;materialize PENDING 分支 :106-110 安静返回;recordFailure :137-140 安静返回 —— 均不抛异常、不误报成功 |
| 0 行时不覆盖现持有者 | ✔ | 三个更新语句的 WHERE 均含 claim_token=? |
| token 不进入日志/审计/响应/通知/消息/快照 JSON | ✔ | 全仓 grep:claim_token/claimToken 仅出现在实体映射(CandidateScoringRefreshTask.java:73-74)、Executor 的 SQL/注释/record、以及 CandidateScoringRefreshWorker.java:91 的传参;refresh 包内无任何含 token 的日志语句 |
3. 自动化复验
| 命令 | 实际 | 预期 | 结论 |
|---|---|---|---|
mvn -Dtest="*CandidateScoringRefresh*Test" test |
49 / 49,F=0 E=0 S=0 | ≥ 49 | ✔ |
| 关联回归(9 组通配符) | 571 / 571,F=0 E=0 S=0,BUILD SUCCESS | ≥ 571 | ✔ |
mvn clean verify |
2176 / 2176,F=0 E=0 S=0,BUILD SUCCESS | ≥ 2176 | ✔ |
python docs\verify_openapi.py |
176 paths / 214 operations,All checks passed | 176/214 | ✔ |
日志:fv-fence.log、fv-regression.log、fv-clean-verify.log
4. 真实 MySQL 最小黑盒
脚本 fv-fence.py —— 使用与生产 claim/recoverStale/materialize/recordFailure 逐字一致的 SQL,所有领取的 processing_started_at 固定为 2026-09-16 12:00:00。
结果:PASS
A. 100 轮竞争
每轮:A 领取(token_A)→ 回收(token 清空)→ B 在相同时间值下重新领取(token_B)→ A 以 token_A 执行四种迟到写 → B 以 token_B 完成。
| 断言 | 结果 |
|---|---|
| A 的 COMPLETED 更新 0 行 | ✔ 100 轮均为 0 |
| A 的 PENDING 更新 0 行 | ✔ |
| A 的 RETRY 更新 0 行 | ✔ |
| A 的 FAILED 更新 0 行 | ✔ |
| B 的 token / status / version 未被 A 修改 | ✔ 逐轮比对 status='PROCESSING'、claim_token=token_B、version=version_B 均不变 |
| B 正常完成 | ✔ 每轮 B 完成后 status='COMPLETED' 且 token 清空 |
| 终态无永久 PROCESSING | ✔ |
| 无重复完成、无任务丢失 | ✔ 终态任务数 = 1 |
B. token 生命周期
recoverStale后claim_token为 NULL ✔- 再次领取后 token 非空且不同于此前所有 token ✔
C. 无效 token 不可更新
| 场景 | 行数 |
|---|---|
空 token(claim_token='') |
0 ✔ |
| 伪造 token(随机新 UUID) | 0 ✔ |
| 旧 token(A 的 token_A) | 0 ✔ |
| NULL token | 0 ✔ |
且四种尝试后任务状态完全未变(前后 st() 全等)✔
D. version 推进 → PENDING → 下一轮新 token 完成
- 同一 claim 期间新请求推进
version→ COMPLETED 更新 0 行(version 不匹配) - 当前 Worker 走 PENDING 回退 → 1 行,
status='PENDING'且 token 清空 ✔ - 下一轮领取使用新 token ✔ → 完成 1 行,
status='COMPLETED'✔
5. 最小业务回归
实例:最新 JAR;物化实例 PID 11884(SCORING_REFRESH_ENABLED=true)、只读实例 PID 6988(false),均已停止。
| 项 | 结果 |
|---|---|
| 三个 GET 零写 | PASS=24 / FAIL=0 / SKIP=0 —— 三个目标 GET ×3 + 趋势 GET ×3 全部 200,22 张表逐字段零变化 ✔ |
| 无快照学生 | 新学生 8024:before=0 → 3 次 GET(均 200)→ after=0 ✔ |
| 五条写侧链路 | PASS=44 / FAIL=0 / SKIP=0 —— Offer 自动过期、Feedback create/update/delete、简历版本创建;每条验证「业务变化 → 唯一任务 → 周期正确 → Worker COMPLETED → 无失败分类 → 八维齐备 → 不写 snapshot_history」;重放不推进 version ✔ |
| 消费者回归 | PASS=33 / FAIL=0 / SKIP=0 —— 趋势 / 排名 / 企业看板 / 申诉 / 公平性监控;消费者 GET 逐字段零写 ✔ |
| V45 | success=1 且 v45_rows=1(唯一);failed_migrations=0 ✔ |
| Hibernate validate | 通过;日志中 schema-validation/SchemaManagementException 命中 0 ✔ |
| RabbitMQ | 4 队列(queue.sms.verification / queue.notification / queue.notification.dlq / queue.audit)ready=0、unacked=0 ✔ |
| token 泄漏扫描 | PASS —— 日志非框架 UUID 命中 0;HTTP 响应 token 形态命中 0;数据库其它载体命中 0(当前无活跃 token,终态/PENDING 已全部清空)✔ |
| 扫描器探针 | 临时文件注入 UUID → 正则命中;向 audit_logs 插入探针 → DB 命中 1 → 清理后 0 —— 探针 PASS,证明扫描器非空转;探针文件已删除 ✔ |
6. 收尾
| 项 | 结果 |
|---|---|
| 本轮 Java 实例 | 仅停止本轮记录的两个 PID(11884、6988);tasklist java.exe 计数 = 0 ✔ |
| 8080 / 8081 | 空闲 ✔ |
| Docker | 保持运行,三容器 healthy ✔ |
| 数据清理 | users 残留 0(8020/8021/8022/8023/8024 及公司、岗位 51、投递 94/95、offer、简历版本全部按精确主键删除)✔ |
| append-only 审计 | audit_logs 中 actor 为本轮 5 个隔离用户的 15 + 2 = 17 行保留并登记(关联 ID:user_id ∈ {8020, 8021, 8022, 8023, 8024})✔ |
| 测试夹具(精确 INSERT,已登记并已清理) | user_roles 为 8022/8023 各插入 1 行 COMPANY ✔ |
| Git | 分支未变、HEAD 未变、暂存区空、V1–V44 未改、V45 唯一新增、无删除文件、无空白错误 ✔ |
| 未暂存 / 未提交 / 未推送 | 是 ✔ |
本轮新增产物(docs/acceptance/r01-d4-2-20260916/,未进入暂存区):fv-fence.py + fv-fence-out.txt、fv-tokenscan.py、fv-fence.log、fv-regression.log、fv-clean-verify.log、v3-fv-out.txt、s1-fv-out.txt、s4-fv-out.txt。
7. PASS / FAIL / SKIP 清单
FAIL:无。
SKIP:无。
PASS:
- 基线(分支/HEAD/暂存区/V1–V44/V45 唯一/最新 JAR)
- 代码审查 12 项(token 原子生成、不复用、COMPLETED 四条件、PENDING/RETRY/FAILED 三条件、四处清空、recoverStale 清空、新 token、
processing_started_at退出所有权判定、0 行 stale 处理、不覆盖、不泄漏) - fence 定向 49/49
- 关联回归 571/571
- 全量 clean verify 2176/2176(S=0)
- OpenAPI 176/214
- 真实 MySQL 100 轮竞争(四种迟到写全 0 行、B 未被修改、B 均完成、无永久 PROCESSING、无丢失)
- token 生命周期(回收后 NULL、重领后新值)
- 空/伪造/旧/NULL token 均无法更新
- version 推进 → PENDING → 新 token 完成
- 三个 GET 零写 24/24(22 表逐字段)
- 无快照学生 0 行
- 五条写侧 44/44
- 消费者 33/33
- V45 成功且唯一
- Hibernate validate
- RabbitMQ 四队列为 0
- token 泄漏扫描 0 + 探针有效
8. 复验过程中的自暴露问题(非产品缺陷)
| # | 问题 | 处置 |
|---|---|---|
| 1 | fv-fence.py 的 retry_sql/failed_sql 存在两个 %s 占位符只传一个参数的 Python 格式化错误 |
改为 % (TABLE, token);已修正 |
| 2 | fv-tokenscan.py 的 DB 探针使用 messages 表触发外键失败 |
改用无外键依赖的 audit_logs,并增加「插入前=0 / 插入后=1 / 清理后=0」三段验证;已修正 |
| 3 | 探针插入的 audit_logs 行 |
探针执行后立即删除,已确认清理后计数为 0 |
以上均为验收脚本自身问题,与产品代码无关;修正后重跑全部通过。
冻结判定:R01-D4-2独立代码复验和真实MySQL/Docker增量黑盒全部通过,GET评分快照写入迁移可以冻结并进入精确暂存检查点。