Cron 不是“定时器”,而是一个带锁的执行系统
很多人把 ir.cron 理解成“每隔几分钟调一次函数”。
但源码里真正的设计目标要严谨得多:
- 多个 worker 不能同时跑同一个任务
- 长任务要能分批执行
- 失败和超时要能区分
- 任务完成后要按状态重新安排下一次执行
所以 cron 不是一个简单的 sleep + call,而是一套“抢锁、执行、汇报、重调度”的闭环。
直接触发和调度线程,走的是不同的交易边界
method_direct_trigger() 是一个很容易被忽略但很关键的方法。
它的意思不是“直接在当前事务里跑完任务”,而是:
- 先刷新当前环境
- 再去尝试获取这个 job
- 然后在独立的执行流里跑服务器动作
如果任务已经被别的 worker 抢走,它会直接报错。 这样就避免了管理员手工点“立即执行”时把同一个 cron 重复跑一遍。
_process_jobs 先筛数据库,再筛任务,再抢锁
调度线程会先找出当前数据库里所有 ready 的任务。 但 ready 并不等于可执行,因为还要过三道关:
- 数据库版本和模块状态要正常
- 任务必须仍然在队列里
- 当前 worker 必须真的抢到锁
_process_jobs_loop() 的核心就是这一点:
它对每个 job 逐个尝试 _acquire_one_job()。
如果事务冲突,说明别的 worker 已经先一步拿走了,当前线程就跳过。
这也是 Odoo cron 能横向扩展的基础。
为什么一个 cron 可以“跑一半再接着跑”
真正执行 job 的是 _process_job() 和 _run_job()。
它们会给 job 建立一个新的 cursor 和环境,然后让 server action 反复运行,直到出现明确的完成状态。
这里最重要的不是“跑了几次”,而是“状态怎么判断”:
- fully done:本轮已经完成,正常延后再跑
- partially done:只处理了一部分,要尽快重跑
- failed:本轮没成功,按失败计数处理
_run_job() 还支持 progress API。
也就是说,长任务可以在每批处理后告诉 cron:
- 我处理了多少
- 还剩多少
- 是否要在结束后停用任务
这比“一口气跑到底”安全得多。
_commit_progress 的意义不是“顺手提交一下”
_commit_progress() 是长任务的关键配套接口。
如果你在 cron 外调用它,它只是普通 commit;
但在 cron 内调用时,它会更新进度记录,让调度器知道当前批次到底做到了哪一步。
这带来两个好处:
- 中途崩溃时,cron 能知道任务不是完全没干
- 任务可以按批次滚动,避免一个事务太大
这也是 Odoo 建议长任务分批处理的根本原因。 不是为了“写法好看”,而是为了事务、锁和超时都更可控。
失败计数和重调度不是摆设
_process_job() 会根据执行结果更新 failure count,并决定怎么重调度。
连续失败会累积,超时也会单独统计。
当失败足够多、时间窗口也够长时,cron 甚至会自动停用。
这不是惩罚开发者,而是保护系统:
- 避免一个坏任务每隔几分钟把 worker 拉死
- 避免同一个失败在日志里无限刷屏
- 避免调度线程被“永远失败的任务”占满
实战建议
如果你在写长 cron,优先记住这三条:
- 不要把所有记录塞进一个超大事务
- 批量处理时要用
_commit_progress()汇报状态 - 出现重复执行时,先查锁和 worker,而不是先怀疑业务代码
理解了锁、进度和重调度,你就能把 cron 从“黑箱定时器”变成可控的后台作业系统。
DISCUSSION
评论区