任务状态显示 PROVIDER_SUBMISSION_UNKNOWN 时,不一定意味着模型服务没有返回结果。一次实际故障证明,供应商已经完成生图,失败发生在结果写入 R2 的最后一步。本文记录如何通过 Attempt 持久化数据还原真实数据流,并修正错误分类和凭据校验。
问题现象
前端收到的任务快照大致如下,敏感标识已替换:
1 | { |
Worker 日志同时出现:
1 | provider submission quarantined |
第一反应通常是重试模型请求。但对没有幂等键或提交查询能力的供应商,盲目重试可能产生重复计费,所以必须先读 Attempt 的内部错误。
排查过程
从任务表追到 Attempt
任务表里的错误是面向用户的摘要,技术原因保存在 video_generation_task_attempt:
1 | SELECT status, provider_job_id, checkpoint, error_code, |
这次 Attempt 的内部错误是:
1 | persisting Volcengine image result: putting private provider artifact: |
这条信息改变了排查方向:供应商提交并不是未知,平台是在保存供应商结果时失败。
还原真实调用链
对于同步返回图片的模型,Worker 的调用链是:
1 | WorkerService.Process |
火山方舟已经返回图片 URL,StoreRemote 成功拿到图片后,PrivateArtifactStore 调用 S3 客户端上传对象。R2 在签名阶段拒绝了凭据,因此图片没有成为平台资产。
检查 R2 凭据格式
Cloudflare R2 的 S3 凭据应区分两个字段:
| 字段 | 典型长度 | 用途 |
|---|---|---|
| Access Key ID | 32 位 | 签名中的访问标识 |
| Secret Access Key | 64 位 | 签名密钥,不应填入 Access Key ID |
数据库里只检查长度和脱敏后的非敏感字段,不要把密钥原文打印到日志:
1 | SELECT provider, endpoint, account_id, bucket, |
本次查询确认 Access Key ID 是 64 位,正好对应 R2 返回的拒绝原因。此前还发现 Endpoint 中的账户 ID 与 Account ID 字段不一致,也需要一并修正。
根因
这里有两个层次的错误。
配置错误:Access Key ID 与 Secret Access Key 填反或填错
R2 根据密钥格式直接拒绝了对象上传。因为上传发生在模型返回图片之后,所以“模型生成失败”不是事实。
代码错误:把所有同步 Generate 错误都当成提交未知
原来的 Submit 对非视频模型统一包装:
1 | result, err := p.Generate(ctx, task) |
这会把三类完全不同的错误混在一起:请求没发出去、供应商明确拒绝、供应商已返回结果但平台入库失败。最后一类被错误地送进人工对账隔离分支。
解决方案
为结果入库错误建立明确类型
在供应商适配器中,图片、文本和同步视频结果写入失败时使用专门的 artifactPersistenceError 包装。它表达一个关键事实:上游结果已经拿到,平台没有完成资产提交。
Submit 遇到该类型时返回:
1 | &domainvideo.SubmissionError{ |
Worker 随后使用 PROVIDER_CHARGED_NO_ARTIFACT 完成失败结算,释放用户预留积分,同时创建需要关注的供应商成本对账记录。它不会把同一请求盲目再提交一次。
校验 R2 配置,尽早阻止凭据填反
管理服务现在在保存和测试 Cloudflare R2 配置时校验:
1 | if len(strings.TrimSpace(input.AccessKeyID)) != 32 { |
同时校验 Endpoint 与 Account ID 是否属于同一 R2 账户。后台的“测试配置”按钮仍然会执行真实的验桶、写入、读取和删除测试;测试通过后再保存并重启 API、Worker。
验证与回归
新增了两组回归测试:
- Provider 收到图片结果后,模拟对象存储失败,确认返回的是
Charged=true且Unknown=false; - Worker 收到已计费的资产入库错误,确认 Attempt 进入
failed,错误码为PROVIDER_CHARGED_NO_ARTIFACT,并创建对账状态。
实际运行的验证命令:
1 | make test |
Go 测试、前端测试、go vet、ESLint、Prettier、TypeScript 检查和构建均通过。修复后的 API 与 Worker 热重载后保持运行。
常见误区与边界
PROVIDER_SUBMISSION_UNKNOWN是一个需要谨慎处理的安全状态,不应该看到它就直接重试。先查 Attempt 的error_message和checkpoint。- 供应商已经返回结果、但本地或对象存储入库失败时,重试生成请求可能造成重复成本。正确做法是修复存储后重新处理,或创建新的用户重试任务。
actual_credits: 0只说明平台没有向用户结算成功结果,不等于供应商一定没有产生费用;后者需要通过对账记录和供应商账单确认。- R2 Endpoint、Account ID、Access Key ID、Secret Access Key 必须来自同一账户。测试通过后仍需重启 API 与 Worker,因为存储客户端在进程启动时构建。
检查清单
- 先查
video_generation_task_attempt的内部错误,不只看任务摘要。 - 确认供应商是否已经返回图片 URL 或其他结果。
- 检查 Access Key ID 与 Secret Access Key 是否填反,长度是否分别为 32 和 64。
- 校验 R2 Endpoint 与 Account ID 是否匹配。
- 先点击对象存储“测试配置”,再保存并重启 API、Worker。
- 只有在确认提交未成功且供应商支持安全重试时,才重新提交同一类请求。