[P0] 素材上传重复提交产生大量重复记录 + HEVC转码后素材永久卡"处理中" #1714

Open
opened 2026-09-05 14:10:44 +08:00 by xiaoxia · 1 comment
Owner

现象(staging 实测 2026-09-05)

用户上传 13 个视频素材,IMG_2282.MOV 建 10 条记录、IMG_2283.MOV 12 条、IMG_2284.MOV 4 条,每 20~50 秒重复一轮,Redis celery 队列堆 39 个 ingest_asset 任务,全部卡 processing。

根因链(完整诊断见附件报告 asset_duplicate_upload_rootcause_20260905.md)

  1. 前端无文件级去重:useAssetUpload.ts 入队不查重直接 push;axios 全局 timeout 仅 10s,complete 含 OSS 检查+DB+派单易超时,后端记录已建成但前端标 error,重试即重复落库;3 个大 MOV 占满 3 个并发上传槽,每轮 OSS 重传 20~50s 轮转。
  2. 后端去重对直传失效:前端 complete 从不传 file_hash,后端唯一去重闸门 if request.file_hash: 不生效;complete 接口无幂等键,每次 prepare 生成新 storage_key。
  3. HEVC 转码孤儿(卡"处理中"根因):worker ingest 转码后把 storage_key 改成 _h264 新 key(ingest.py:568),再 find_by_storage_key(新key)(:662)查不到原占位 asset,走兜底再建一条 READY 新记录,原 PROCESSING 占位成永不更新的孤儿;前端 3s 轮询永不停止。

修复要求

前端 P0:文件指纹(name+size+lastModified)入队去重;complete 传 file_hash/幂等 token;complete 超时不得盲目重传(改轮询确认),complete 请求放宽超时;处理中/上传中禁止重复提交,重试仅限真失败;素材列表轮询设最大时长(10min)超时标失败。
后端 P0:complete 接口按 file_hash/幂等 token 幂等,短时重复直接返回已存在 asset,不新建记录不重复派单。
后端 P1:worker 转码后用原 asset id 关联回写占位 asset,禁止按新 storage_key 查不到就新建 READY 记录。
清理 P2:staging 重复 processing 孤儿记录 + 队列任务,修复上线后清理(删除前列清单确认)。

单测必备:重复 complete 幂等、转码后不产生重复 READY、前端同文件不重复入队。

## 现象(staging 实测 2026-09-05) 用户上传 13 个视频素材,IMG_2282.MOV 建 10 条记录、IMG_2283.MOV 12 条、IMG_2284.MOV 4 条,每 20~50 秒重复一轮,Redis celery 队列堆 39 个 ingest_asset 任务,全部卡 processing。 ## 根因链(完整诊断见附件报告 asset_duplicate_upload_rootcause_20260905.md) 1. **前端无文件级去重**:useAssetUpload.ts 入队不查重直接 push;axios 全局 timeout 仅 10s,complete 含 OSS 检查+DB+派单易超时,后端记录已建成但前端标 error,重试即重复落库;3 个大 MOV 占满 3 个并发上传槽,每轮 OSS 重传 20~50s 轮转。 2. **后端去重对直传失效**:前端 complete 从不传 file_hash,后端唯一去重闸门 `if request.file_hash:` 不生效;complete 接口无幂等键,每次 prepare 生成新 storage_key。 3. **HEVC 转码孤儿(卡"处理中"根因)**:worker ingest 转码后把 storage_key 改成 _h264 新 key(ingest.py:568),再 find_by_storage_key(新key)(:662)查不到原占位 asset,走兜底再建一条 READY 新记录,原 PROCESSING 占位成永不更新的孤儿;前端 3s 轮询永不停止。 ## 修复要求 **前端 P0**:文件指纹(name+size+lastModified)入队去重;complete 传 file_hash/幂等 token;complete 超时不得盲目重传(改轮询确认),complete 请求放宽超时;处理中/上传中禁止重复提交,重试仅限真失败;素材列表轮询设最大时长(10min)超时标失败。 **后端 P0**:complete 接口按 file_hash/幂等 token 幂等,短时重复直接返回已存在 asset,不新建记录不重复派单。 **后端 P1**:worker 转码后用原 asset id 关联回写占位 asset,禁止按新 storage_key 查不到就新建 READY 记录。 **清理 P2**:staging 重复 processing 孤儿记录 + 队列任务,修复上线后清理(删除前列清单确认)。 单测必备:重复 complete 幂等、转码后不产生重复 READY、前端同文件不重复入队。
Author
Owner

2026-09-06 复查更新:两层残留根因 + celery 孤儿任务(修复方案补充)

前端 #1716、后端 complete 幂等、队列隔离 #1722 已合并部署后,staging 实测仍有问题。用户实测反馈:重复文件会被跳过但每次都重新上传一遍 OSS;全新视频(内容全新)也会被系统误判重复跳过。经 DB/Redis/代码三方实测,定位两层残留根因。

残留问题 1:prepare 阶段完全不去重(重复文件白传一遍 OSS)

  • 现状:prepare_direct_upload(apps/api/app/api/routes/upload.py 约 241 行)只签 OSS 表单,file_hash 查重只在 complete 阶段发生。重复文件每次都完整上传 OSS 之后才在 complete 被跳过,浪费带宽且长时间占用上传槽。
  • 后端修复
    1. prepare 请求接收 file_hash(DirectUploadPrepareRequest 加可选字段,向后兼容)
    2. prepare 阶段先按 file_hash 查重:命中重复直接返回 duplicated=trueskip_transfer=trueasset_id=<已有素材>,不签 OSS 表单
    3. 未命中则预建一条 processing 占位 asset(client_upload_id 幂等),占住闸门,后续 complete 必须关联该占位
    4. Response 新增 duplicated / skip_transfer / asset_id 三个可选字段
  • 前端修复
    1. prepare 响应 skip_transfer=true 时直接短路:跳过 OSS 直传和 complete 调用,用返回的 asset_id 直接入库展示
    2. completeDirectUpload 调用必须补传 file_size(当前漏传,是残留问题 2 的诱因之一)

残留问题 2:同名兜底去重误杀全新视频(用户反馈"新视频被系统判重"的真根因)

  • 现状:_find_duplicate_asset(upload.py 约 115-176 行)第三级"同名兜底":同素材库 + 同名 + uploading/processing 状态 + 30 分钟窗口内(FALLBACK_DEDUP_WINDOW_MINUTES=30)即判重。find_recent_active_by_library_and_name(asset_repository.py 约 480 行)的大小校验写法是 if file_size and file_size > 0 才加 file_size 过滤条件——而 direct complete 调用处没有传 file_size(=0),大小校验被整体跳过。iPhone 文件名 IMG_xxxx.MOV 会循环复用,导致 30 分钟内上传同名但内容/大小全新的视频被误判重复跳过。
  • 实证(staging DB):用户 11:15-11:17 上传 6 个 MOV(2277/2282/2283/2284/2285/2286),file_hash 全部不同、各一条、全库 0 条重复 hash;其中 IMG_2285.MOV 有一条僵尸 processing 记录(file_size=0,03:16:10 创建)占住同名,新视频即被误杀。
  • 修复四原则(防误杀优先于防漏判)
    1. file_hash 非空时跳过同名兜底(hash 才是硬证据,同名只是弱信号)
    2. 同名兜底必须 file_size > 0 且大小一致才允许判重
    3. file_size = 0 / 未传时直接放行,绝不允许判重
    4. repository 层 find_recent_active_by_library_and_name 同步严格化:file_size 为 0/None 时返回 None
  • complete_direct_upload 必须把前端传入的 file_size 透传到查重逻辑。

残留问题 3:celery 孤儿任务(素材卡"处理超时")

  • 实证:ingest_job 02eed0d9ef6f4ee78013bbd4d560ae9d(celery task 0d78a68e-ad24-47ef-9d84-49ef75fdddb3)03:16:10 起 processing 永不回写,同批 8 个任务全部秒级 completed;worker 日志无该任务任何执行记录;Redis LLEN transcode/generation = 0unacked HLEN = 1 —— worker 取走消息后未执行、未回写,成孤儿消息(高度关联 #1722 队列隔离部署 / worker 重启时在途消息丢失或路由到不存在的队列)。
  • 修复要求
    1. celery 配置 task_acks_late=True + 合理的 visibility_timeout,worker 重启后未完成任务自动重新投递
    2. processing 超时兜底:ingest_job / asset 的 processing 状态超过阈值(建议 10 分钟)自动转 failed,asset 同步标记失败、前端允许用户重试,不允许永久挂 processing
    3. worker 启动时扫描 stuck processing 的在途任务,做恢复或置 failed
    4. 排查 #1722 队列路由配置,确认任务不会被路由到已不存在的队列
  • 单测必备:任务卡死 → 超时自动转 failed;worker 重启 → 在途任务可恢复/重投。

数据清理(修复全部上线后执行,删除前列清单经确认)

  • staging 约 69 条重复 processing 记录 + 约 110 条 E2E 测试 processing 记录 + IMG_2285.MOV 僵尸记录(asset id 7f55841665474368991ad75e3b46d2f3 / ingest_job 02eed0d9ef6f4ee78013bbd4d560ae9d)。

验收标准

  1. 重复文件:prepare 阶段即命中,不再上传 OSS,前端直接展示已有素材
  2. 全新视频(即使与库内 processing 素材同名、file_size 不同/hash 不同):正常上传,绝不被误判重复
  3. 手动制造任务卡死:10 分钟内自动转 failed,前端可重试
  4. worker 重启:在途任务自动重投完成,不产生 unacked 孤儿
## 2026-09-06 复查更新:两层残留根因 + celery 孤儿任务(修复方案补充) > 前端 #1716、后端 complete 幂等、队列隔离 #1722 已合并部署后,staging 实测仍有问题。用户实测反馈:**重复文件会被跳过但每次都重新上传一遍 OSS;全新视频(内容全新)也会被系统误判重复跳过**。经 DB/Redis/代码三方实测,定位两层残留根因。 ### 残留问题 1:prepare 阶段完全不去重(重复文件白传一遍 OSS) - 现状:`prepare_direct_upload`(apps/api/app/api/routes/upload.py 约 241 行)只签 OSS 表单,file_hash 查重只在 complete 阶段发生。重复文件每次都完整上传 OSS 之后才在 complete 被跳过,浪费带宽且长时间占用上传槽。 - **后端修复**: 1. prepare 请求接收 `file_hash`(DirectUploadPrepareRequest 加可选字段,向后兼容) 2. prepare 阶段先按 file_hash 查重:命中重复直接返回 `duplicated=true`、`skip_transfer=true`、`asset_id=<已有素材>`,不签 OSS 表单 3. 未命中则预建一条 processing 占位 asset(client_upload_id 幂等),占住闸门,后续 complete 必须关联该占位 4. Response 新增 `duplicated` / `skip_transfer` / `asset_id` 三个可选字段 - **前端修复**: 1. prepare 响应 `skip_transfer=true` 时直接短路:跳过 OSS 直传和 complete 调用,用返回的 asset_id 直接入库展示 2. `completeDirectUpload` 调用必须补传 `file_size`(当前漏传,是残留问题 2 的诱因之一) ### 残留问题 2:同名兜底去重误杀全新视频(用户反馈"新视频被系统判重"的真根因) - 现状:`_find_duplicate_asset`(upload.py 约 115-176 行)第三级"同名兜底":同素材库 + 同名 + uploading/processing 状态 + 30 分钟窗口内(FALLBACK_DEDUP_WINDOW_MINUTES=30)即判重。`find_recent_active_by_library_and_name`(asset_repository.py 约 480 行)的大小校验写法是 `if file_size and file_size > 0` 才加 file_size 过滤条件——而 direct complete 调用处**没有传 file_size(=0),大小校验被整体跳过**。iPhone 文件名 IMG_xxxx.MOV 会循环复用,导致 30 分钟内上传同名但内容/大小全新的视频被误判重复跳过。 - 实证(staging DB):用户 11:15-11:17 上传 6 个 MOV(2277/2282/2283/2284/2285/2286),file_hash 全部不同、各一条、全库 0 条重复 hash;其中 IMG_2285.MOV 有一条僵尸 processing 记录(file_size=0,03:16:10 创建)占住同名,新视频即被误杀。 - **修复四原则(防误杀优先于防漏判)**: 1. `file_hash` 非空时**跳过同名兜底**(hash 才是硬证据,同名只是弱信号) 2. 同名兜底必须 `file_size > 0` **且大小一致**才允许判重 3. `file_size = 0` / 未传时直接放行,绝不允许判重 4. repository 层 `find_recent_active_by_library_and_name` 同步严格化:file_size 为 0/None 时返回 None - complete_direct_upload 必须把前端传入的 file_size 透传到查重逻辑。 ### 残留问题 3:celery 孤儿任务(素材卡"处理超时") - 实证:ingest_job `02eed0d9ef6f4ee78013bbd4d560ae9d`(celery task `0d78a68e-ad24-47ef-9d84-49ef75fdddb3`)03:16:10 起 processing 永不回写,同批 8 个任务全部秒级 completed;worker 日志无该任务任何执行记录;Redis `LLEN transcode/generation = 0` 但 `unacked` HLEN = 1 —— worker 取走消息后未执行、未回写,成孤儿消息(高度关联 #1722 队列隔离部署 / worker 重启时在途消息丢失或路由到不存在的队列)。 - **修复要求**: 1. celery 配置 `task_acks_late=True` + 合理的 `visibility_timeout`,worker 重启后未完成任务自动重新投递 2. **processing 超时兜底**:ingest_job / asset 的 processing 状态超过阈值(建议 10 分钟)自动转 failed,asset 同步标记失败、前端允许用户重试,不允许永久挂 processing 3. worker 启动时扫描 stuck processing 的在途任务,做恢复或置 failed 4. 排查 #1722 队列路由配置,确认任务不会被路由到已不存在的队列 - 单测必备:任务卡死 → 超时自动转 failed;worker 重启 → 在途任务可恢复/重投。 ### 数据清理(修复全部上线后执行,删除前列清单经确认) - staging 约 69 条重复 processing 记录 + 约 110 条 E2E 测试 processing 记录 + IMG_2285.MOV 僵尸记录(asset id `7f55841665474368991ad75e3b46d2f3` / ingest_job `02eed0d9ef6f4ee78013bbd4d560ae9d`)。 ### 验收标准 1. 重复文件:prepare 阶段即命中,**不再上传 OSS**,前端直接展示已有素材 2. 全新视频(即使与库内 processing 素材同名、file_size 不同/hash 不同):正常上传,绝不被误判重复 3. 手动制造任务卡死:10 分钟内自动转 failed,前端可重试 4. worker 重启:在途任务自动重投完成,不产生 unacked 孤儿
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: xiaoxia/xiaoxia-saas#1714