这是系列第四篇。第二篇介绍执行主链路,第三篇讨论数据与并行表达。本篇围绕同一条教学视频链路检查故障窗口,源码固定到 5cded0b 版本,仍限定普通非流式、非关键路径提前完成模式。文中的中断与恢复场景属于源码推演,没有启动引擎或注入实际故障。
一次超时,为什么无法判断业务是否执行
引擎向转码服务发出请求,等待一段时间后收到超时异常。此时至少存在两种可能:请求没有到达服务;请求已被接收,甚至已经生成文件,只是响应没有回来。站在调用方看,两种情况都可能表现为一次失败,但业务副作用完全不同。
若统一重新发送,第一种情况能够得到补救,第二种情况却可能重复计算、覆盖文件或重复计费。工作流重试解决的是再次尝试的机会,不能直接回答上一次是否真正执行过。这个不确定性存在于引擎与执行器之间,也存在于完成通知的反向链路中。
因此,讨论可靠性前应分别说清三个对象:引擎认为任务处于什么状态,执行器认为业务作业处于什么状态,外部存储中已经出现了什么结果。它们不是一个共同提交的数据库事务。任意一方暂时不可访问,都可能造成不同观察者看到不同进度。
对视频案例,最有价值的排障问题不是“有没有重试功能”,而是“这次重试会不会再次执行同一分片,原来的结果还能不能查到,哪个结果有资格被合并”。后面的状态、锁和幂等设计,都需要回到这些具体问题。
状态和上下文分别保存什么
运行状态描述流程和任务的推进位置,例如待执行、运行中或成功;上下文保存后继调用需要的数据。两者相关,但不能互相替代。一个成功状态不包含文件是否还存在的全部信息,一份结果地址也不能独立证明引擎已经完成该节点的状态更新。
RuntimeStorage同时实现图信息与上下文存储接口,连接运行存储及相应的换出路径。更底层的 DAGInfoDAO与 ContextDAO分别组织图、任务和上下文的读写。分析时应沿着实际调用继续看,不能因为都使用 Redis 就认定这些操作自动组成一个事务。
这里还有两个不同层次的持久性问题。应用调用保存接口,是程序层面的动作;数据能否在存储进程崩溃、主从切换或保留期结束后继续存在,还依赖部署配置与运维策略。本文能够核对保存与读取代码,不能据此承诺任何部署下都不丢状态。
上下文也不宜承担无限期审计档案的职责。它主要服务当前执行的数据传递,业务如果需要追查输入版本、输出校验或人工处理原因,应明确保存周期及独立记录位置。这是应用设计建议,并不意味着引擎没有日志,而是日志和业务证据具有不同用途。
例如,一个分片产生了两个结果地址,当前上下文只保留其中一个。恢复处理需要知道另一个结果是谁生成的、是否已被引用,以及能否清理。仅查看当前状态快照无法回答全部问题,必要的业务作业记录必须在执行过程中建立。
加锁和状态检查保护了哪些操作
DAGTraversal在流程对应的锁内判断可运行节点,并先保存被选中任务的 READY 状态。另一个完成事件再触发遍历时,可以看到节点已离开尚未开始状态,从而减少同一节点被重复选中的机会。这是正常并发推进中的协调机制。
FunctionTaskRunner在任务完成路径使用任务对应的锁,并重新读取任务信息。随后,AbstractTaskRunner的完成校验拒绝尚未进入可完成阶段或已经终结的节点。重复完成通知进入这条路径时,并非无条件再次写入成功结果;调用方也不能把被拒绝的通知一律当作需要无限重试的新故障。
锁保护的对象必须说清。它能够约束使用同一存储过程和锁键的引擎操作,却不会替远端转码服务锁住输出文件,也不能阻止一个已经受理的作业继续运行。引擎将节点标记失败,不代表外部计算已经被撤销。
RedisDistributedLocker通过脚本进行加锁与解锁,带有获取超时和过期参数。判断临界区安全性时,还要考虑锁有效期、持有时间和异常退出。仅看到类名中包含“分布式锁”,不足以证明任意长耗时操作都被持续排他保护;本文也未对锁过期竞争进行运行验证。
更实用的做法是给每条保证写清作用范围:流程锁协调任务选择,任务锁协调完成处理,业务幂等保护外部副作用。三个范围可以相互配合,但不会因为其中一处加锁,就自动覆盖另外两处。
沿着故障窗口检查保证范围
把一次正常执行拆成可观察的动作,会更容易看到中断可能发生的位置:
选择任务 → 保存 READY → 派发请求
↓
执行器产生结果
↓
收到完成通知 → 保存输出上下文
↓
保存完成状态
↓
再次推进
图中的箭头表示顺序,不表示跨系统原子提交。尤其是完成处理,函数 runner 在普通路径中先写输出映射和上下文,再保存任务状态。这样安排有助于后继在看见成功状态时取得输出,但两次写入之间仍然可能发生异常。
下面按源码路径梳理需要核对的场景。“业务补充”是建议,不是已内置能力清单:
| 故障位置 | 源码路径中的事实 | 保证边界 | 业务补充 |
|---|---|---|---|
| 保存 READY 后、派发前退出 | 选中状态先被保存 | 不能据此证明自动补发 | 检查滞留任务及重新驱动入口 |
| 远端受理后响应丢失 | 派发异常可进入失败处理 | 失败不等于业务未执行 | 用业务键查询或复用结果 |
| 执行完成但回调丢失 | 存在超时检查接入路径 | 需核对配置与通知链路 | 通知重试、作业查询与对账 |
| 上下文写入后状态写入失败 | 数据与状态分步保存 | 可能出现进度不一致 | 重读双方状态再决定处理 |
| 同一通知重复到达 | 锁内重新读取并校验状态 | 不覆盖外部重复副作用 | 明确重复通知的终止条件 |
超时检查也要区分“存在入口”与“已经覆盖全部故障”。DAGOperations和 TimeCheckRunner提供相应组织路径,实际何时登记、检查后怎样处理,以及进程重启后如何继续,必须结合配置和完整调用链验证。本文不会把一个定时检查接口描述为完整恢复协议。
部分分片成功时,故障判断还需要保留粒度。第二个分片失败,不等于已完成的第一和第三个结果都应作废;但若处理参数版本发生改变,原结果也未必可继续复用。是否重用由业务结果的身份与版本决定,不能只比较节点状态。
这张表的目的,是让“可靠”变成可以逐项审查的问题。没有证据的自动补偿、自动恢复或业务只执行一次,不应填成肯定答案。源码可解释的机制和部署中尚待验证的性质,应保留在不同栏目中。
进程退出后,第一步应确认退出发生在什么位置。如果 READY 已保存,但没有派发记录,仍不能仅凭日志缺失断言请求没有发送:日志本身也可能来不及落盘。若执行器提供稳定业务键查询,就能把推测收敛为可核查的作业状态;没有查询能力时,这个窗口只能依靠幂等重试或人工判断,无法从引擎快照中凭空恢复事实。
输出写入与完成状态写入之间也不能直接清空上下文重来。写入的结果可能已被其他读取者观察,重试也可能再次产生新的地址。恢复动作应先保存现场,再判断当前结果能否复用;需要覆盖时,明确覆盖的是同一版本的重算结果还是新业务版本。这样的顺序有助于保留追查依据,却仍属于应用侧恢复流程,而非本文证明的引擎原子性。
重试如何与业务幂等配合
SimpleRetryPolicy根据失败状态、次数和条件决定是否再次尝试;具体公式与次数含义已在第二篇说明。这里更重要的是重试的身份:同一个业务操作可以有多次调用尝试,两种标识不应该混用。
下面是一份应用侧的教学记录,不是引擎原生字段格式。示例只用于说明业务键与尝试标识的区别:
{
"businessKey": "video-A:segment-1:h264-v2",
"attemptId": "attempt-03",
"resultVersion": "h264-v2",
"status": "processing"
}
稳定业务键表示“对哪个输入按哪份参数产生什么结果”;尝试标识表示“这次调用是哪一次”。如果每次重试都把随机请求编号当作幂等键,执行器就会把所有重试当作新业务。如果只有稳定键却没有尝试信息,又很难解释旧请求为什么在新请求之后完成。
执行器可以围绕稳定键维护处理中、已完成和失败记录。遇到重复请求时,返回已有结果、已有作业标识,或明确告知仍在处理。检查与创建记录需要业务存储中的原子约束,先查询再无条件写入并不能防止两个请求同时穿过检查。
结果提交同样需要约束。如果同一业务键的新旧尝试都能写结果,应明确哪一份输出有效,并在提交时检查版本或有效尝试。这种保护常被称为防止旧执行者继续写入;是否使用租约、版本比较或条件更新,应由执行器的存储能力决定,不能从引擎终态检查推导出它已经存在。
迟到回调尤其需要谨慎。旧尝试在任务重新运行后才返回,此时节点可能再次处于运行状态。检查“当前不是终态”只能判断流程阶段,未必能识别通知属于哪一次尝试。需要结合回调携带的信息核对完整路径;若协议不足,应在业务适配层补充,而不是声称所有迟到结果都被自动排除。
幂等记录还有生命周期。保留时间短于可能的重试与迟到窗口,就可能在记录过期后再次执行;不同参数却误用同一个键,则可能错误复用旧结果。幂等是围绕业务身份、原子提交和保留期共同建立的约束,不是给请求增加一个字符串即可完成。
从状态恢复到业务恢复
固定版本中存在可核对的重新执行路径:DAGOperations.redoTask先调用 DAGRunner.resetTask,再提交遍历。重置过程会处理选中任务及后继的状态,并重新组织相关子任务信息。它证明代码提供了重新驱动入口,不证明所有滞留执行都会被自动发现并调用该入口。
还应注意重置范围。源码会将指定子任务名归一到相应祖先任务,并递归处理后继,因此不能简单理解成“只重发这一个最小分片”。调用之前必须确认将影响哪些计算、已完成结果是否复用,以及上下文中的旧值会怎样参与后续执行。
这条重置代码没有顺带完成所有外部副作用的撤销。文件、计费、通知和业务数据库写入有各自的生命周期。恢复人员如果只盯着任务状态,可能让流程重新成功,却留下重复产物或重复通知。重新执行之前先查询执行器和产物,是一种更可审查的恢复顺序。
对于无法自动判断的情况,可以把执行暂时交给人工:保存原始输入版本、当前状态和外部结果证据,明确选择复用、重做或终止,再记录处理原因。人工处理不是简单修改一个状态字段;它也需要遵守依赖和结果完整性,否则后继可能在缺少产物时继续。
建议用四个问题评审恢复方案:谁发现异常,凭什么判断真实进度,谁有权改变状态,改变后怎样验证业务结果。每个问题都应指向代码入口或业务责任人。没有答案的部分先列为接入缺口,不用“支持恢复”四个字掩盖。
演练时也应同时观察引擎和执行器:任务最终成功只是其中一项,产物数量、版本、是否重复通知和是否留下悬挂作业都应被核对。只有这些业务结果一并满足约束,才能认为这次恢复达到了预期。本文给出的是演练问题与源码依据,实际恢复时间和成功率需要在具体部署中测量。
源码阅读日期为 2026-09-23。可从完成处理、存储接口实现和重试测试源码交叉检查本文结论;测试文件中的断言仅作为源码证据,本篇未运行这些测试。前一篇是数据映射、分支与并行汇合,下一篇将把这些边界落实到执行器接入协议与运行治理。