文章以 Go、SQLite 媒体生成服务为例,区分审核失败与生成请求提交状态不明,避免任务长期占用并发额度。客户额度、并发槽位和外部费用预留分别管理,释放本地资源时保留不确定费用记录,避免盲目重试付费请求。
安全检查可能在媒体服务商收到生成请求之前就失败。你的队列需要单独表示这种状态,将它与生成请求是否提交成功尚不确定的状态区分开。否则,即使根本没有可供轮询的生成任务,用户的并发名额也可能一直被占用。
我们在运营 Rendastic 时遇到了这个问题。这是一个使用 Go 和 SQLite 开发的应用,会向外部服务商提交视频和图片生成任务。在自动内容检查无法继续返回可用结果后,几个任务一直停留在待确认状态。重启应用后,这些任务依然卡在那里。重新发送同一个付费 POST 请求,又可能导致第二次扣费。
下面介绍我们对状态模型做出的调整,以及如何在不生成媒体内容的情况下测试这些调整。
一个请求可能预留三种不同的资源:
这些预留资源各有不同的生命周期。当必需的安全检查无法完成时,我们可以将客户任务标记为失败,并释放前两项资源。外部请求仍有可能已经产生费用,因此,对应的请求流水记录和费用尚未确定的预算预留都保持不变。
释放客户的可用额度,并不能证明服务商已经退还了任何费用。在后续核对成本时,这一区别很重要。
我们原有的通用不确定状态处理流程,适用于可能已经到达服务商的媒体生成请求。但对于安全检查失败、又没有可供轮询的任务标识符的情况,这套流程并不够用。
我们引入了一种带类型的内容审核不可用错误,并为内容检查设置了明确的超时时限。下面用伪代码说明判断逻辑:
if required_check_is_unavailable:
withhold_unchecked_result()
fail_customer_job_and_release_customer_reservation()
preserve_original_provider_journal()
elif main_generation_submission_is_uncertain:
hold_customer_job_for_reconciliation()
preserve_original_request_key()
第一个分支处理的是技术故障:超时、传输失败,或无法使用的安全判定结果。明确的策略拒绝属于另一种业务结果,应当有独立且向用户说明的处理方式。
无论走哪个分支,未经检查的结果都不能开放下载。状态不确定的外部请求必须保留原有身份标识;绝不能仅仅为了摆脱不确定状态,就换一个新 key 重试。
客户任务状态的变更,以及配额或积分的释放,必须在同一个数据库事务中完成。否则,如果进程在这两步之间崩溃,就可能出现任务已经失败却仍占用配额,或者任务仍在运行、积分却已经退回的情况。
这个操作还必须具备幂等性。两个 worker 可能观察到同一次失败,维护操作也可能在重启后执行。应通过终态检查,确保重复结算既不会退还两次积分,也不会改变已经结算的结果。
SQLite 事务在这里很有用,因为任务准入、本地配额和积分账本都存放在同一个数据库中。如果系统使用多个独立服务,就需要具备同等作用的对账机制,不能假定跨服务的写入会一起提交。
我们将历史任务的恢复范围限制为:能够明确识别、已经长时间未完成的内容审核请求,且这些请求没有任务 ID,也没有尚未解决的主媒体生成请求。只要有证据表明媒体生成请求已经被接受,或者提交状态仍不确定,任务就继续留在对账流程中。
这样的范围限制,避免了将所有长期待处理的任务都视为可以安全释放资源或重新执行。我们对比了恢复前后的服务商请求流水,确认修复客户侧状态既没有创建新的外部请求,也没有抹掉服务商侧尚未确定的费用。
本地模拟服务商可以返回超时、待处理响应、有效的拒绝结果,或已完成响应。关键是验证以下行为:
只测试生成成功的情况,无法覆盖这些边界。一次有效的验收测试,应当明确中断客户侧状态与服务商侧状态可能出现分歧的每一个阶段。
披露:我运营 Rendastic,我们在其中实现了这套恢复流程。本文描述的是我们应用的行为,并不保证其他服务商会如何计费或作出审核决定。本文草稿由 AI Agent 生成,并已对照我们的实现和已记录的测试进行核查。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。