Cron

Odoo Cron 不只是“每隔几分钟跑一次”:锁、进度和重调度链路讲透

读懂 ir.cron 如何抢锁、分批执行、提交进度和重调度,才能避免把长任务写成一次性炸库。

Odoo 开发 框架
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

Cron 不是“定时器”,而是一个带锁的执行系统

很多人把 ir.cron 理解成“每隔几分钟调一次函数”。 但源码里真正的设计目标要严谨得多:

  • 多个 worker 不能同时跑同一个任务
  • 长任务要能分批执行
  • 失败和超时要能区分
  • 任务完成后要按状态重新安排下一次执行

所以 cron 不是一个简单的 sleep + call,而是一套“抢锁、执行、汇报、重调度”的闭环。

直接触发和调度线程,走的是不同的交易边界

method_direct_trigger() 是一个很容易被忽略但很关键的方法。 它的意思不是“直接在当前事务里跑完任务”,而是:

  • 先刷新当前环境
  • 再去尝试获取这个 job
  • 然后在独立的执行流里跑服务器动作

如果任务已经被别的 worker 抢走,它会直接报错。 这样就避免了管理员手工点“立即执行”时把同一个 cron 重复跑一遍。

_process_jobs 先筛数据库,再筛任务,再抢锁

调度线程会先找出当前数据库里所有 ready 的任务。 但 ready 并不等于可执行,因为还要过三道关:

  1. 数据库版本和模块状态要正常
  2. 任务必须仍然在队列里
  3. 当前 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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。