提交更新
This commit is contained in:
79
backend-java/docs/stale-task-finalize-guidelines.md
Normal file
79
backend-java/docs/stale-task-finalize-guidelines.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Stale 任务收尾约束(公共心跳模块)
|
||||
|
||||
> 适用范围:所有使用 [TaskHeartbeatService](../src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java) 的 file_task 模块
|
||||
> (PRODUCT_RISK_RESOLVE / PRICE_TRACK / SHOP_MATCH / PATROL_DELETE / QUERY_ASIN /
|
||||
> APPEARANCE_PATENT / SIMILAR_ASIN / DELETE_BRAND)。
|
||||
|
||||
## 历史故障(2026-06-08)
|
||||
|
||||
SIMILAR_ASIN taskId=13102 的 file_job 长期持有 RUNNING 等待 Python 凑齐
|
||||
batchSize 行再发 Coze。Python 端 13:20 异常掉线,但任务到下午 16 点仍卡在 12% 不前进,
|
||||
30 分钟兜底失效。
|
||||
|
||||
链路:
|
||||
|
||||
1. [resetStuckJobs](../src/main/java/com/nanri/aiimage/modules/task/service/TaskResultFileJobWorker.java)
|
||||
每 60 秒扫一次,把 RUNNING 超 30 分钟的 file_job 改回 PENDING。
|
||||
2. worker 立刻把它捞起来重跑 `processResultFileJob`。
|
||||
3. 重跑分支命中 "等 Python 上传更多 row" → 调用 `touchJavaSideTaskActivity`,
|
||||
把 `file_task.updated_at` 刷成 now。
|
||||
4. `finalizeStaleTasks` 用 `lt(updated_at, threshold)` 预过滤,于是这个任务
|
||||
永远进不到候选集,Redis 心跳判定那一步根本走不到。
|
||||
|
||||
→ Python 已断线 N 小时,任务仍在循环,永不收尾。
|
||||
|
||||
## 约束
|
||||
|
||||
### 1. stale-finalize 必须以 Redis 心跳为主信号
|
||||
|
||||
`TaskCacheService.touchTaskHeartbeat` 写的 Redis key 仅在 **Python 真实活动**
|
||||
(心跳接口、上传 result chunk、提交 row)时刷新。Java 内部循环不应触碰它。
|
||||
|
||||
stale 判定时应:
|
||||
|
||||
- 以 Redis heartbeat 为主信号;
|
||||
- 仅在 Redis 不可用 / 缺失时回落到 `file_task.updated_at`;
|
||||
- 不要再用 `lt(file_task.updated_at, threshold)` 做预过滤
|
||||
(除非该模块 100% 不存在长期 RUNNING 等待循环)。
|
||||
|
||||
参考实现:[SimilarAsinTaskService.finalizeStaleTasks / isHeartbeatStale](../src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java)
|
||||
和 [AppearancePatentTaskService](../src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java) 中 P1-7 注释段。
|
||||
|
||||
### 2. 不要在"等 Python"分支刷 file_task.updated_at
|
||||
|
||||
如果模块的 file_job 会持有 RUNNING 状态等待外部上传更多数据,
|
||||
**这条等待路径上不允许调 `setUpdatedAt(now)` 类方法**。
|
||||
|
||||
否则会触发 [resetStuckJobs](../src/main/java/com/nanri/aiimage/modules/task/service/TaskResultFileJobWorker.java)
|
||||
→ 重跑 → 续命 → 再 reset 的死循环。
|
||||
|
||||
如必须刷 `updated_at` 推动其他逻辑,应迁移到独立字段(例如
|
||||
`file_job.updated_at`、Redis pendingScope 字段),与 stale 判定信号解耦。
|
||||
|
||||
### 3. 新增/修改 stale-finalize 流程的 PR 检查清单
|
||||
|
||||
- [ ] stale 判定的主信号是 Redis 心跳(不是 `file_task.updated_at`)
|
||||
- [ ] 列出该模块所有刷 `file_task.updated_at` 的代码位置,
|
||||
其中没有任何一处会被 `processResultFileJob` 在"等 Python"分支重复触发
|
||||
- [ ] 如果 file_job 会持有长 RUNNING(pull-style 工作流),明确写明 stuck-reset
|
||||
重跑后的幂等行为(不会续命 updated_at)
|
||||
- [ ] 给 finalize 路径补一个 debug 接口(参考
|
||||
[DebugTaskRecoveryController](../src/main/java/com/nanri/aiimage/modules/debug/controller/DebugTaskRecoveryController.java)),
|
||||
绕过过滤直接对单 task 强制收尾,便于线上应急
|
||||
|
||||
## 当前各模块状态(2026-06-08 复核)
|
||||
|
||||
| 模块 | stale 入口 | 是否曾出现自我续命 | 是否需要改 |
|
||||
|---|---|---|---|
|
||||
| SIMILAR_ASIN | `SimilarAsinTaskService.finalizeStaleTasks` | 是(已修) | 已切 Redis-first |
|
||||
| APPEARANCE_PATENT | `AppearancePatentTaskService.finalizeStaleTasks` | 是(已修) | 已切 Redis-first |
|
||||
| PRODUCT_RISK_RESOLVE | `DeleteBrandStaleTaskService.failStaleProductRiskResolveTasks` | 否 | 当前不需要 |
|
||||
| PRICE_TRACK | `DeleteBrandStaleTaskService.failStalePriceTrackTasks` | 否 | 当前不需要 |
|
||||
| SHOP_MATCH | `DeleteBrandStaleTaskService.failStaleShopMatchTasks` | 否 | 当前不需要 |
|
||||
| PATROL_DELETE | `DeleteBrandStaleTaskService.failStalePatrolDeleteTasks` | 否 | 当前不需要 |
|
||||
| QUERY_ASIN | `DeleteBrandStaleTaskService.failStaleQueryAsinTasks` | 否 | 当前不需要 |
|
||||
| DELETE_BRAND | `DeleteBrandStaleTaskService.failStaleDeleteBrandTasks` | 否 | 当前不需要 |
|
||||
| BRAND | `BrandTaskService.failStaleRunningTasks`(独立心跳源) | 否 | 当前不需要 |
|
||||
|
||||
下次新增"等 row 凑批" / "pull-style 长 RUNNING file_job"型功能时,
|
||||
请先读本约束。
|
||||
Reference in New Issue
Block a user