失败不是没有文件,而是没有等到文件
这次问题出在一条视频自动化链路的最后阶段。前面的素材、字幕和合成准备都已经完成,到了渲染环节,调用方连续重试,最后仍然失败。表面上的报错是没有找到视频文件,现场却并不是“视频没有生成”:渲染服务已经接受了请求,只是返回的不是文件路径,而是一个任务号。
当时的调用方只会处理一种响应:从同步返回的结果里找到视频路径,然后把这个路径交给后续合成。如果返回里直接有路径,链路就继续;如果只有任务号,解析器就像什么也没看见一样,最终报出没有视频路径。现场为了交付,只能人工进入任务目录定位已经生成的文件,再继续字幕烧录和合成。
这件事让我先停下了“是不是渲染失败”的判断。**异步任务最危险的误判,是把“请求已提交”当成“产物已生成”。**提交成功只说明服务收到了工作,不说明下游现在已经拥有可读取的文件。
同步和异步其实是两种状态
我一开始看到调用命令返回成功,很自然地想从输出中继续找路径。后来发现,问题不在路径字段的名字,而在于响应本身代表了不同状态。
同步响应里带着可用文件路径,说明服务已经完成了足够多的工作,调用方可以立即做存在性校验并返回。异步响应只带任务号,说明工作还在另一个执行阶段,调用方此刻拿到的是“如何找到结果”的凭证,而不是结果本身。这两个响应不能共用同一条结束逻辑。
如果不把这两种状态分开,调用方只能在两件事之间摇摆:要么把异步响应错误地当成空结果,要么不加判断地继续重试。前者会漏掉已经在进行的任务,后者可能重复创建渲染任务,造成资源浪费,也让真正的失败原因更难追踪。
**任务号不是缺失的文件路径,而是状态机进入下一阶段的信号。**识别这个信号以后,处理方式就清楚了:有文件,校验后返回;只有任务号,进入等待;两者都没有,立即保留明确错误。
最容易误判的捷径是盲目重试
这次最容易走的错误捷径,是看到提交命令成功,却立刻把它当成产物已经可用;找不到路径时,再重新提交几次。它看上去符合“失败就重试”的经验,但并不适合异步服务。
因为第 1 次提交可能已经创建了任务,第 2 次重试并不会让第 1 个任务更快完成,反而可能创建第 2 个、第 3 个重复任务。更麻烦的是,调用方没有记录自己究竟在等待哪个任务,后续即使目录里出现多个视频,也无法确定哪一个对应当前输入。
我选择先解析响应类型,再决定下一步,是因为这一步不会增加外部副作用,却能避免把未知状态扩大成重复执行。同步响应不额外轮询,异步响应不重新提交,而是围绕已拿到的任务号查询状态。这样既保留了原来的快速路径,也把等待责任放在了正确的位置。
这里的原则不只适用于视频。上传、导出、报表生成、模型训练,只要服务可能先返回任务号,调用方就不能用“命令返回成功”替代“结果已经可读”。
轮询必须是只读的等待
拿到任务号以后,真正需要补上的不是一个无边界的循环,而是一段有明确含义的只读轮询。它要做的事情很少:按任务号读取当前状态,判断是否进入终态,从终态信息里提取结果位置,再校验这个位置确实指向本地文件。
只读很重要。轮询命令不能重新提交任务,不能修改任务状态,也不能用一个“等待并顺便触发”的接口替代查询。这样即使轮询执行多次,也不会把等待动作变成新的生产动作。自动化链路要把“查询进展”和“创建工作”分成两个不可混淆的动作。
状态读取也不能只看某一行输出就结束。一次查询可能返回状态变化、日志和最终结果,解析时应该优先看终态信息,并按约定顺序寻找结果字段。如果主字段没有值,可以接受明确的备用字段;但无论字段叫什么,最后只接受经过验证的本地绝对路径,并确认文件确实存在。
这个校验看似保守,却挡住了另一类隐患。一个字符串看起来像路径,不代表文件已经落盘;一个结果地址看起来完整,也不代表它属于当前任务。如果跳过存在性校验,后面的合成阶段才会报错,错误位置就会从渲染边界漂移到更远的地方,排查成本反而更高。
终态、超时和真空响应要分别处理
异步分支至少要区分 3 种结果。第 1 种是成功终态:拿到结果位置,文件校验通过,返回给下游。第 2 种是失败或超时:任务没有在规定时间内得到可用结果,带着任务号明确报错。第 3 种是真空响应:既没有文件路径,也没有任务号,说明提交方连后续追踪所需的凭证都没有给出来。
这 3 种情况不能都压缩成“没有视频”。成功终态需要继续处理,超时需要告诉人应该查哪个任务,真空响应则应该尽早暴露接口契约问题。尤其是超时,如果只返回一个模糊的超时提示,现场仍然要重新猜测是否存在孤儿任务;错误里保留任务号,才有可能做后续定位或人工兜底。
等待还必须有间隔和上限。间隔太短会给状态服务增加无意义压力,太长又会让链路显得卡住;没有超时则会把一个异常任务变成永远不结束的进程。我更愿意把间隔和超时做成可以覆盖的参数,并给出稳定默认值,因为不同渲染时长可能不同,但“最终必须结束并说明原因”不能靠环境运气。
如果不处理这些边界,自动化最糟糕的结果不是立即失败,而是一直处于“好像快完成了”的状态。它既不交付,也不给出可行动的错误,最后只能依赖人工盯目录。
用离线测试固定状态机边界
修复后,我没有只拿一条真实任务去验证。真实任务耗时长,还会把外部服务、文件目录和网络状态混在一起,无法证明每个分支都按预期工作。因此我把外部调用收在一个可替换的接缝里,用离线测试分别模拟同步成功、异步成功、超时和真空响应。
同步测试要证明:返回已有文件时立即结束,不启动轮询。这个断言很关键,因为兼容异步不等于把所有任务都改成等待,原本的同步路径应当保持原有速度和行为。异步成功测试要证明:拿到任务号后只读查询,直到终态出现文件,并且文件校验通过才返回。超时测试要证明:等待到上限后会带任务号明确失败,而不是无限循环。真空响应测试则要证明:仍然保留原有的明确错误,不把接口缺陷吞掉。
这些测试的价值不只是覆盖代码行。它们把状态机的边界写成了可重复的约束:什么情况下立即返回,什么情况下等待,什么情况下必须停止。以后有人调整轮询间隔、结果字段或命令参数,先看这些边界,就不容易为了“让测试变绿”而破坏调用语义。
下次我会怎么做
下次遇到类似的自动化接口,我会先问 4 个问题:这次返回的是结果还是任务凭证?如果是任务凭证,哪个查询动作是只读的?什么字段代表终态结果,文件如何校验?等待多久必须停止,错误里要保留什么定位信息?
然后我会把流程写成一条简单的状态机:解析响应;有路径就校验并返回;只有任务号就按间隔只读轮询;终态有文件则再次校验;超时就带任务号失败;响应为空就保留契约错误。同步路径不被异步逻辑拖慢,异步路径也不靠人工去目录里捞文件。
这件事让我重新意识到,自动化的稳定不是“命令多重试几次”,而是每个中间状态都有人负责解释。**一个流程成熟的标志,不是它能在顺利时跑通一次,而是它在等待、失败和返回不完整时仍然知道下一步是什么。**把任务号当成状态机入口,把轮询限制为只读,把终态文件当成真正交付条件,再给等待设置边界,链路才算真正稳住。