幽灵闭环递归:日记 cron 连续两天执行但文件未生成
本文最后更新于 2026年11月6日 凌晨
10 月 22 号早上打开 Obsidian,日记页面连续两天是空的。
不是那种”今天没写”的空,是 cron 日志里明明白白写着 20 号和 21 号各跑了一次、exit code 都是 0 的空。脚本跑了,没报错,没产出。像一个员工打卡上班了,坐了一天工位,什么活没干,下班时还跟你说了声”今天任务完成了”。
我管这种状态叫幽灵闭环——整条链路看起来是闭合的,每一步都返回了成功,但信息流在中间某个环节被吞掉了。
第一次发现:日志说它跑了
翻 cron 日志的时候我盯着那两行看了很久。
1 | |
时间对,命令对,没有 error 关键字。但 ~/notes/daily/ 目录下,10 月 20 和 21 号的文件压根不存在。
我第一反应是”是不是路径写错了”,手动 bash -x daily-note.sh 跑了一遍。脚本确实执行了所有步骤:拉数据、格式化、写文件。但最后 ls 一看不对——文件写到了一个临时目录,然后被 rm -rf 清掉了。
翻代码才想起来,上个月加过一个”草稿暂存”逻辑:如果数据源返回的字段不完整,就把内容写到临时目录做标记,等下次再合并。问题是,”字段不完整”的判断条件写得太宽松了,那两天数据源刚好返回了一个非标准格式的日期字段,触发了这个分支。
更恶心的是,这个分支的最后一行是 exit 0。
幽灵闭环是怎么形成的
把整条链路拆开看:
数据源返回数据 → 脚本解析 → 格式校验 → 写入目标目录 → exit 0
问题出在第四步之前。格式校验失败后,脚本没有走”报错退出”的路径,而是走了”暂存到临时目录”的路径。这个路径本身没有 bug——它确实完成了”暂存”这个动作。但从调用者的视角看,脚本跑完了,没报错,一切正常。
这就是幽灵闭环的结构特征:每一个环节都认为自己完成了职责,但职责之间有一个缝隙没人负责。 格式校验的职责是”判断数据是否可用”,它判断完了,返回了”不可用”。写入的职责是”把可用数据写进去”,它没收到数据,所以没写。两个环节都没错,但数据就这么消失了。
exit 0 是最后一根稻草。它告诉 cron”我成功了”,cron 就安心了。没有告警,没有重试,什么都没有。
手动修完之后,我改了三层东西
修完那个判断条件之后,日记恢复了。但我知道同样的问题还会在别的地方出现。”数据源返回了个我没见过的格式”这种事,概率不是零。
我做了三层改动,从下往上。
第一层:产出断言。 脚本最后加了一段硬检查:
1 | |
-s 同时检查文件存在且非空。如果目标文件没出现,脚本直接 exit 1。cron 会记录失败,我的监控脚本会收到通知。
这一层解决的是”静默空转”问题。不管中间哪个环节出了问题,只要最终产出物不在,脚本就不会假装成功。
第二层:心跳文件。 在脚本最开头写一个时间戳文件:
1 | |
然后有一个独立的 cron job 每 15 分钟检查一次:如果心跳文件超过 30 分钟没更新,就发告警。
这一层解决的是”脚本根本没跑”的问题。比如 cron 本身挂了、磁盘满了、依赖的某个服务没启动导致脚本在 source 阶段就卡死了。这些情况下,脚本内部的断言根本执行不到。心跳文件放在最开头,只要脚本进程启动了就一定会写。
第三层:死信队列。 之前那个”暂存到临时目录”的逻辑,我改成了追加写到一个 dead-letter.log 文件里,带上时间戳和原始数据。
1 | |
这样即使数据校验失败,原始数据也不会丢。我可以事后排查,而不是像这次一样只能靠翻 cron 日志和回忆代码来推断。
为什么之前没发现
回头看,这个 bug 其实有迹可循。10 月 19 号晚上数据源就返回过一次非标准格式,我手动跑脚本时发现文件没生成,当时以为是”那天没数据”,没深究。
如果当时多问一句”为什么没数据?是数据源没返回,还是脚本判断成不可用了?”,问题就能提前一天暴露。
但人不是这么工作的。早上打开日记发现空白的瞬间,大脑的第一反应是”哦今天没写”,而不是”不对劲,昨天也没写”。认知上的确认偏误比技术上的 bug 更难修。
现在跑起来是什么感觉
改完之后跑了两周,稳定。心跳文件和产出断言各触发过一次告警——一次是服务器重启后 cron 没自动恢复(心跳告警),一次是数据源临时返回了空数组(产出断言)。两次都是分钟级发现,分钟级处理。
对比之前:两天空白,靠肉眼发现,靠翻代码定位。
幽灵闭环最可怕的地方不是它会造成多大的损失,而是它让你觉得系统很健康。日志绿绿的,cron 安安静静,一切看起来都在正常运转。直到你打开那个本该有内容的页面,发现它空了。