Files
crawler-plugin/backend-java/docs/stale-task-finalize-guidelines.md
2026-06-08 18:31:55 +08:00

80 lines
4.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 会持有长 RUNNINGpull-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"型功能时,
请先读本约束。