定时任务显示成功却没执行:Linux文件锁与中断恢复的排查方法
定时脚本每天触发,不代表每天的业务都执行过。一个容易被忽略的情况是:保活进程先拿到了浏览器锁,主任务遇到锁冲突后直接以退出码 0 结束。服务管理器记录“成功”,实际动作却一次也没运行。下面用 Linux 文件锁和持久化执行记录说明怎样区分这种情况。
1. 先区分两类进程的等待策略
保活是机会型任务:浏览器正在执行主任务时,这轮保活可以跳过。每日主任务则有明确的完成要求,遇到同一把锁时应在有限时间内等待;超时仍未拿到锁,要向调度器返回失败。把两者都写成非阻塞抢锁,并在冲突时返回成功,会把“未执行”混入“已完成”。
等待时间没有适用于所有业务的固定值。可以先测量保活实际占锁时间,再给主任务设置一个上限,例如 180 秒。该上限还必须小于服务总超时,否则进程可能刚拿到锁就被服务管理器终止。
2. 有界等待的最小实现
下面的例子运行于 Linux,锁文件目录需要由运行用户提前创建。进程间互斥依赖的是内核文件锁,不能用“文件是否存在”判断是否有人持锁。
import fcntl
import time
from contextlib import contextmanager
@contextmanager
def exclusive_lock(path, wait_seconds=180):
with open(path, "a", encoding="utf-8") as handle:
deadline = time.monotonic() + wait_seconds
acquired = False
try:
while not acquired:
try:
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
acquired = True
except BlockingIOError:
if time.monotonic() >= deadline:
raise TimeoutError("browser lock unavailable")
time.sleep(0.2)
yield
finally:
if acquired:
fcntl.flock(handle, fcntl.LOCK_UN)
超时计算使用 time.monotonic(),避免系统校时影响等待长度。无论业务函数抛出异常还是正常返回,文件描述符都会关闭。不要在仍有进程可能持锁时删除并重建锁文件:新旧进程可能锁住不同的 inode,互斥就失效了。
3. 把业务结果写成可恢复的记录
锁只解决“不要同时操作浏览器”,并不解决“提交后断线还能否重试”。一次外部操作至少有三个关键时刻:准备执行、已开始可能产生外部影响的操作、已取得业务确认。应在点击提交前持久化第二个时刻,而不是等待成功页面出现后才记日志。
例如创建工单时,请求可能已经到达服务端,但浏览器在跳转前断开。下轮启动看到“提交已开始、结果未确认”,应该先查工单列表或服务端幂等键。只有证据表明没有产生提交,才允许重新执行。直接把超时解释为失败并重发,会制造重复工单。
记录键可由账号、业务对象和执行周期组成。账号是隔离边界,周期是去重边界;昨天的完成记录不应阻止今天的任务,本周未确认的提交则不应因为换了一天就被重复发送。
4. 不让只读检查覆盖执行事实
建议分别保存“最近一次执行”和“最近一次审计”。假设上午主任务漏跑,下午健康检查成功刷新了页面,如果恢复程序只读取一个统一的 latest 文件,就可能把这次审计误当成业务已经执行。执行报告应包含 run ID、启动时间、退出状态和外部操作记录;审计报告只反映观察时刻的平台状态。
多个进程写 JSON 时,临时文件也要唯一。固定使用 report.json.tmp 会产生覆盖竞争。可在目标目录用 tempfile.mkstemp 创建独立临时文件,写入、刷新后再用 os.replace 原子替换。多写者仍需互斥或版本检查,原子替换本身不会防止旧报告覆盖新报告。
5. 如何证明修复有效
- 先让辅助进程持锁,启动主任务,确认它确实等待;释放后只执行一次。
- 让持锁时间超过上限,确认主任务返回非零,并保留可诊断的超时信息。
- 在外部提交后模拟进程中断,确认重启走对账分支,而不是再次提交。
- 运行只读审计,确认恢复程序所读的执行时间没有被刷新。
验收最终应落到业务系统:对象是否真的创建、状态是否真正完成、是否存在重复记录。服务退出码和 HTTP 200 都是中间证据,不能代替业务确认。
- 点赞
- 收藏
- 关注作者
评论(0)