自动化脚本把自己重试死第三次之后,我们把观察和执行拆开了

上个月二点档的发布任务,同一个 bug 报了三次。第一次,脚本自建自清理;第二次,它把异常吞掉变成空结果;第三次,它自己探测自己写的文件,探测结果反过来又变成重试条件,空转了四十分钟。直到我们把"判断该不该重试"从执行进程里搬出去,这种活锁才停下来。记录一下。

专属插画
自动化脚本把自己重试死第三次之后,我们把观察和执行拆开了

自动化脚本把自己重试死第三次之后,我们把观察和执行拆开了

上个月二点档的发布任务,同一个 bug 报了三次。第一次,脚本自建自清理;第二次,它把异常吞掉变成空结果;第三次,它自己探测自己写的文件,探测结果反过来又变成重试条件,空转了四十分钟。直到我们把"判断该不该重试"从执行进程里搬出去,这种活锁才停下来。记录一下。

三次死法

第一次很干脆:执行脚本在 V4 入库这一步全量失败,schema 冲突,catch 住异常、清理临时目录、exit 0,日志一行红字。当时失败了,但排查路径是通的。

第二次更隐蔽:同一段代码,这次异常在翻译转换层被包装后返回成了空对象,执行方把空对象当成正常结果,既没有 diff 也没有报错。任务显示"完成",报告里正常记录了三语审核。这次是假成功,比失败难发现得多。

第三次才打出房间:脚本每轮轮询都会重建临时目录,并且把上一轮的失败输出写回收查文件;复查逻辑发现复查文件存在就再次触发清理,清理又回填复查文件。执行进程头部的判断和尾部的动作咬住了自己,空转四十分钟,每轮报告都写同一句"清理完成"。

排查时卡住的地方

最卡住的不是定位,是沉默:任务显示完成、报告留了记录、监控没有任何异常。真正的信号藏在报告外面——复查文件的修改时间比触发时间还早。同一条规则在文件里出现了两次,一次是判断条件,一次是动作产物。

更尴尬的是,当时写"一键清理"这个功能的需求就是我提的,理由很简单:一条命令把上一轮残留清干净。需求本身没毛病,但观察者和执行者住在同一个进程里,观察到的永远是执行结果的回声——这是一个只反映自身状态的传感器。这个结构坑我们不是第一次踩,最后一次结算是用了整整一个黄金时段。

护栏设计

拆完之后才好谈修复。拆开是一次性的,结构才能慢慢改:

**观察和触发拆开。** 临时文件由写入方在原子写时更新完成标记;重试方只读完成标记做判断,只读就永远不会重新触发动作。

**每份临时产物带 owner 和 TTL。** 不同来源的文件不再互相覆盖;TTL 内 owner 没刷新,不管它是不是还活着,下个阶段一律跳过,交给独立的过期回收。

**空结果与错误分道。** 转换层不许把异常包装成空对象;空结果必须走独立的 diff 分支,报告里要么有正文,要么有一段 diff 说明为什么是空。

**报告加一栏"变更数"。** 执行了但什么都没变的轮次,这栏是零。零不一定错,但它应该被人看了一眼,而不是被"任务完成"三个字盖过去。

风险面暂时用三条路由层的覆盖率兜底(这三条判断后来也全量拆到了独立进程)。改完之后连跑三天,同类空转只有一次苗头,且被"变更数为零"的告警拦在了读者看见之前。当天真正需要变更的那一轮,diff 里能指出具体的 schema 字段,不是空转。

一句话

如果你的执行脚本能自己验证自己的产出,它就能给自己签发通行证。验证者必须独立于执行者,观察也必须独立于执行——谁都不许住在自己造出来的信号里。

留言区

欢迎分享你的想法!

发表留言

0/500

加载留言中…