You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

13 KiB

R01-D4-2 claim fence 定向整改报告(不可复用 claim token)

日期:2026-09-16 分支:refactor/r01-structure-performance HEAD:b10c4a732740e42353ca826f63141c3dd2a49915(整改全程未变) 来源:docs/r01-d4-2-final-independent-incremental-verification-20260916.md 的 FAIL 项(R-1) 整改范围:仅修复 fence 唯一性,未进入其他 R01 项目


1. 结论

R01-D4-2 claim fence 定向整改完成,待最终独立增量复验;GET 评分快照写入迁移尚未冻结。

采用任务书指定的不可复用 claim token 方案。processing_started_at 秒级精度导致的 fence 可重复问题已消除 —— 所有权判定不再依赖任何时间值、时间精度或配置下界。


2. 修复前失败测试(TDD 证据)

新增 CandidateScoringRefreshClaimTokenTest(13 个测试),在只加入 V45 列、未改 Java时运行:

命令:mvn -Dtest="CandidateScoringRefreshClaimTokenTest" test 日志:docs/acceptance/r01-d4-2-20260916/fence-before.log

结果:Tests run: 10, Failures: 6

任务书要求复现的三条,全部复现成功:

要求 测试方法 失败信息
① 两次领取拥有完全相同的 processing_started_at sameTimestamp_staleWorkerPendingRevert_mustNotTouchNewClaim 断言 assertEquals(SAME_INSTANT, processingStartedAt(taskId)) 通过,证明两次领取时间值相同
② Worker B 已重新领取 同上 断言 assertEquals("PROCESSING", status(taskId)) 通过
③ Worker A 的 PENDING 回退可以错误修改 B 的任务 同上 expected: <PROCESSING> but was: <PENDING> ← 核心缺陷复现

同时复现了失败登记的同类覆盖: sameTimestamp_staleWorkerFailure_mustNotTouchNewClaim → expected: <PROCESSING> but was: <RETRY>

其余 4 条失败为 claim_token 尚未写入(值为 null)。

关键点:测试使用常量 SAME_INSTANT = LocalDateTime.of(2026, 9, 16, 12, 0, 0),两次领取传入完全相同的时间值,未使用任何 sleep 或跨秒等待。


3. 实现

3.1 迁移 V45

mydreamjob-server/src/main/resources/db/migration/V45__add_scoring_refresh_claim_token.sql

ALTER TABLE candidate_scoring_refresh_tasks
    ADD COLUMN claim_token VARCHAR(36) NULL
    COMMENT '本次领取的不可复用令牌(UUID),终态/待处理时清空;仅用于并发所有权判定';
  • 仅新增列,未增加索引 —— 查询依据:claim_token 只出现在既有的按主键 id 定位的 UPDATE 中(WHERE id=? AND ...),不参与扫描或排序,因此无需新索引。
  • 未修改 V1–V44,未修改业务表数据,未新增公网接口或 ErrorCode。

3.2 CandidateScoringRefreshTaskExecutor

操作 关键改动
claim 生成 java.util.UUID.randomUUID().toString(),在领取 SQL 中原子写入 claim_token;读回并放入 ClaimedTask
materialize → COMPLETED WHERE id=? AND status='PROCESSING' AND claim_token=? AND version=?;SET claim_token=NULL
materialize → PENDING 回退 WHERE id=? AND status='PROCESSING' AND claim_token=?;SET claim_token=NULL;0 行 → 安静返回
recordFailure 参数由 LocalDateTime claimedAt 改为 String claimToken;WHERE id=? AND status='PROCESSING' AND claim_token=?;SET claim_token=NULL;0 行 → 安静返回
recoverStale SET claim_token=NULL(回收即释放所有权)
ClaimedTask 新增 String claimToken;保留 claimedAt 但仅用于超时扫描与观测,不再参与任何 WHERE 条件

processing_started_at 的用途收敛为:超时扫描(WHERE status='PROCESSING' AND processing_started_at < ?)与观测。所有权证明完全由 claim_token 承担。

3.3 语义保持

  • 同一 claim 期间出现新请求 → version 前进 → COMPLETED 更新 0 行 → PENDING 回退(claim_token 未变,闸门放行)✔
  • 新 Worker 已重新领取 → claim_token 已变 → 完成、失败、回退全部更新 0 行并安静退出 ✔
  • 进入 PENDING / COMPLETED / RETRY / FAILED 均清空 claim_token ✔
  • 不依赖 Thread.sleep、时间精度或配置下界 ✔

4. 测试覆盖(任务书清单逐项)

CandidateScoringRefreshClaimTokenTest(13 个测试,全部通过):

任务书要求 测试方法
同一时间戳下两次领取的 claim token 不同 twoClaimsAtSameInstant_getDifferentTokens
旧 Worker 不能完成新 claim sameTimestamp_staleWorkerPendingRevert_mustNotTouchNewClaim(含完成尝试)
旧 Worker 不能将新 claim 回退 PENDING 同上(核心断言)
旧 Worker 不能记录新 claim 失败 sameTimestamp_staleWorkerFailure_mustNotTouchNewClaim
当前 Worker 可以正常完成 currentHolder_canCompleteNormally
同一 claim 期间 version 前进会正确回到 PENDING requestDuringSameClaim_versionAdvances_completionFallsBackToPending
stale recovery 后生成新 token staleRecovery_generatesNewTokenOnNextClaim
terminal/PENDING 状态 token 被清空 terminalAndPendingStates_clearClaimToken、failedTerminalState_clearsClaimToken
双 Worker 循环至少 100 次无覆盖、无永久 PROCESSING doubleWorkerLoop_100Rounds_noOverwrite_noStuckProcessing_noLostRequest
token 不进入日志、审计、响应或错误信息 claimToken_doesNotLeakIntoAuditOrNotificationTables、claimToken_rowNeverExposedInTaskStatusQueriesUsedByObservability

补充:claim_repeatedOnDifferentTasks_producesDistinctTokens、tokenIsUuidShaped_andNotDerivedFromIds(UUID 形态、不可复用、不由 ID 派生)。

全部使用固定时间值,无 sleep、无跨秒等待。


5. 回归

命令 结果
mvn -Dtest="*CandidateScoringRefresh*Test" test 等 7 个 fence 相关类 49 / 49,F=0 E=0 S=0
关联回归(9 组通配符) 571 / 571,F=0 E=0 S=0
mvn clean verify 2176 / 2176,Failures=0、Errors=0、Skipped=0,BUILD SUCCESS
python docs\verify_openapi.py 176 paths / 214 operations,All checks passed

较整改前(2163)新增 13 个测试,全部为 claim token 相关。

过程中修正的测试侧问题(非产品缺陷):

  1. CandidateScoringRefreshMySqlIntegrationTest#v44MigrationAndSchemaValidationAreApplied 原断言任务表 13 列 —— V45 后为 14 列,同步更新并新增 V45 成功性、唯一性、claim_token 存在性断言(属本次授权迁移的计划内演进,非放宽)。
  2. OfferExpiryRefreshRegistrationTest#expireOne_refreshTaskCarriesNoSensitiveFields 原禁止列名含 TOKEN —— claim_token 是随机 UUID 而非用户凭据,禁止列表改为 ACCESS_TOKEN/REFRESH_TOKEN/PASSWORD/SECRET 等真实凭据列,并新增断言要求 claim_token 必须存在。
  3. 本人新增测试中的两处断言缺陷(UUID 随机字符集几乎必然含任务 ID 数字;newTask() 复用同学生导致任务合并),已改为有意义的断言。

6. 真实 MySQL 最小增量复验

使用本轮 clean verify 产出的最新 JAR。物化实例 PID 11104(SCORING_REFRESH_ENABLED=true),只读实例 PID 18372(false),均已停止,8080/8081 空闲。

6.1 V45 迁移与 schema validation

version | description                     | success
44      | candidate scoring refresh tasks | 1
45      | add scoring refresh claim token | 1
v45_rows = 1         failed_migrations = 0
claim_token | varchar(36) | YES
task_cols = 14

Hibernate ddl-auto=validate 通过,日志中 schema-validation/SchemaManagementException 命中 0 ✔

6.2 claim fence 100 轮竞争(固定相同 processing_started_at)

脚本 iv-fence-loop2.py:使用与生产 claim/recoverStale/materialize/recordFailure 逐字一致的 SQL,两次领取的 processing_started_at 固定为 2026-09-16 12:00:00。

轮次 = 100(两次领取的 processing_started_at 固定为 2026-09-16 12:00:00)
[PASS] 100 轮:A 的完成/失败/回退全部 0 行、未覆盖 B、B 均完成、无永久 PROCESSING

每轮断言:

  • A 用旧 token 的完成 / 失败 / PENDING 回退 → 受影响行数均为 0
  • A 未改动 B 的 status、claim_token、attempts、last_error_code
  • B 用新 token 完成 → 1 行
  • 终态无残留 PROCESSING

(脚本首轮运行暴露了自身缺陷:ROW_COUNT() 跨 docker exec 连接失效、回收步骤的 WORKER_TIMEOUT 被误判为污染;已修正后重跑。)

6.3 三个 GET 零写

PASS=24 / FAIL=0 / SKIP=0 —— 三个目标 GET ×3 + 趋势 GET ×3 全部 HTTP 200,22 张表逐字段零变化 ✔

6.4 无快照学生

新学生 8019:before=0 → 3 次 GET(均 200)→ after=0 ✔

6.5 五条写侧链路

PASS=44 / FAIL=0 / SKIP=0 —— Offer 自动过期、Feedback create/update/delete、简历版本创建;每条均验证「业务变化 → 唯一任务 → 周期正确 → Worker COMPLETED → 无失败分类 → 八维齐备 → 不写 snapshot_history」;相同输入重放不推进 version ✔

6.6 消费者最小回归

PASS=33 / FAIL=0 / SKIP=0 —— 趋势(当前周期 2026-09)、排名(rank/percentile/cohortSize)、企业看板 4 端点 + 越权 403、公平性 5 端点(抑制语义保留)、申诉取证非空;消费者 GET 逐字段零写 ✔

6.7 RabbitMQ

4 个队列(queue.sms.verification / queue.notification / queue.notification.dlq / queue.audit)ready=0、unacked=0 ✔

6.8 token 不泄露

  • 测试断言:token 不出现在 audit_logs / notifications / messages / candidate_dimension_scores.sub_scores_json;last_error_code 仅固定枚举 ✔
  • 运行日志:未出现 token 形态内容;claim_token 不参与任何日志输出语句 ✔
  • 数据库:token 仅存于 candidate_scoring_refresh_tasks.claim_token,且在终态/PENDING 时被清空 ✔

7. 精确修改文件

生产代码(3 个)

文件 改动
db/migration/V45__add_scoring_refresh_claim_token.sql 新增:仅加 claim_token VARCHAR(36) NULL
candidatescore/refresh/CandidateScoringRefreshTaskExecutor.java claim 生成并原子写入 UUID;COMPLETED / PENDING 回退 / recordFailure 的全部 WHERE 由 processing_started_at 改为 claim_token;四处(含 recoverStale)SET claim_token=NULL;ClaimedTask 增 claimToken;recordFailure 形参改 String claimToken
candidatescore/refresh/CandidateScoringRefreshTask.java 实体新增 claimToken 映射;processingStartedAt 注释标明不再作为所有权证明

配置/接口/数据:未修改 application.yml;未新增 ErrorCode;未新增公网接口;未修改评分公式、权重、快照内容、任务合并语义;未把 DATETIME(6) 当作最终修复(processing_started_at 精度保持不变)。

测试(新增 1 个类;适配 4 个既有类)

新增:CandidateScoringRefreshClaimTokenTest(13 个测试) 适配:CandidateScoringRefreshMySqlIntegrationTest(列数 13→14 + V45 断言)、OfferExpiryRefreshRegistrationTest(禁止列名调整 + claim_token 存在性断言)、CandidateScoringRefreshClaimFenceTest 与 CandidateScoringRefreshInfrastructureTest(recordFailure 传 claimToken())、CandidateScoringRefreshWorkerLoopIsolationTest(mock 签名 + ClaimedTask 构造)


8. Git 状态

分支:refactor/r01-structure-performance
HEAD:b10c4a732740e42353ca826f63141c3dd2a49915(未变)
暂存区:空
git add -A / commit / push:均未执行
V1–V44:未修改
新增迁移:V45(唯一本轮新增;V44 为 D4-1 产物)
删除文件:无
git diff --check:无空白错误

验收数据已按精确主键清理(users 残留 0:8015/8016/8017/8018/8019 均删除;公司 33/34、岗位 50、投递 92/93、offer、简历版本、申诉及关联行全部清除)。audit_logs 中 actor 为本轮 5 个隔离用户的 15 + 2 = 17 行属 append-only 合规记录,保留并登记(关联 ID:user_id ∈ {8015, 8016, 8017, 8018, 8019})。

唯一测试夹具(精确 INSERT,已登记并已清理):user_roles 为 8017/8018 各插入 1 行 COMPANY。

Docker 保持运行(三容器 healthy);仅停止本轮记录 PID 的实例;8080/8081 空闲。


9. 明确声明

  • 未暂存、未提交、未推送。
  • 未进入其他 R01 工作。
  • 原始验收产物继续排除在后续提交之外。
  • 已按要求「先补失败测试 → 保存修复前失败结果 → 再改生产代码」。

R01-D4-2 claim fence 定向整改完成,待最终独立增量复验;GET 评分快照写入迁移尚未冻结。