Cron 分批执行

Odoo 定时任务为什么不能一口气跑到天亮:`_commit_progress()`、分批执行与 worker 让渡

很多人写 Odoo 定时任务时,只想着“把数据处理完”,却忽略了调度器本身的运行契约。基于 Odoo 19 的 `ir_cron.py` 源码,本文讲清 `_commit_progress()`、剩余时间、分批循环与部分完成状态,解释为什么一个健康的 cron 不是“尽量久地跑”,而是“持续可恢复地跑”。

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

先说结论

Odoo 的 cron 设计目标,从来不是“这次触发后把所有数据一次跑完”。

真正的设计是:

每次跑一小批,明确提交进度,如果还没做完,就尽快安排下一轮继续。

所以你如果把 cron 写成一个超长大循环:

  • 中间不提交进度
  • 不告诉系统还剩多少
  • 一直占着 worker 不放

那它看起来像“认真工作”,实际却是在和调度器对着干。


_commit_progress() 干的不是“记个日志”

ir_cron.py 里的 _commit_progress() 文档写得很直白:

  • processed:这一步处理了多少条
  • remaining:还剩多少条
  • deactivate:这次跑完后是否停用 cron

最关键的是它内部真的会:

  1. ir.cron.progress
  2. self.env.cr.commit()
  3. 返回当前这一轮 cron 还剩多少秒可以继续跑

也就是说,这不是一个“装饰性 API”,而是一个调度契约

很多人误解成:

  • 只是为了后台页面显示进度
  • 不调用也没事

其实不对。

不调用它,系统就更难判断:

  • 你到底有没有做成事
  • 还要不要立刻续跑
  • 这次异常算完全失败,还是“虽然报错,但前面已经推进了一部分”

为什么 Odoo 要强调“部分完成”

_run_job() 的逻辑,会发现 Odoo 并不把执行结果简单分成“成功 / 失败”。

它至少区分:

  • FULLY_DONE
  • PARTIALLY_DONE
  • FAILED

这个区别非常重要。

比如一个 cron:

  • 这轮本来要处理 10000 条
  • 你先顺利处理了 500 条,并 _commit_progress(processed=500, remaining=9500)
  • 然后第 501 条报错

Odoo 不会简单把它当成“这轮全白干了”。

如果进度已经提交过,系统会倾向于把它当作:

做成了一部分,但还没做完。

这样下一轮可以从剩余数据继续,而不是永远卡在“要么全成,要么全死”的二元思路里。


_commit_progress() 返回剩余时间,这一点经常被忽略

源码最后返回:

  • cron_end_time - time.monotonic()

翻成人话:

你这轮 worker 还剩多少预算时间。

所以最稳的写法不是“我先把 while True 跑满”,而是:

  • 每处理一批就提交进度
  • 看看返回的剩余秒数
  • 如果时间不多了,就主动收手,把剩余工作留给下一轮

这比盲目跑到底健康得多。

因为调度器本来就允许你“分期还债”,没必要一口气还清把自己憋死。


_process_jobs_loop() 也说明了一个事实:每个 job 之间要释放锁

调度器外层在 _process_jobs_loop() 里:

  • 逐个获取 ready job
  • 调用 _process_job()
  • 每个 job 结束后 cron_cr.commit()

这说明 Odoo 想要的是:

  • 一个 job 完了,锁释放
  • 下一份 job 有机会拿到执行权
  • 不要让一个任务无限霸占整个 cron 通道

所以“长任务分批化”并不是锦上添花,而是和调度器整体吞吐量直接相关。


_run_job() 的循环语义:允许多跑几轮,但不是纵容你无限跑

源码里有个关键注释:

  • 至少跑到最小轮数 / 最小时间预算
  • 但会在“完全完成”或“失败”后停下来

也就是说,Odoo 给了 cron 一个短时连续推进的机会,但前提是你要配合 progress API。

如果你每轮都:

  • 能处理一部分
  • 明确写出 done / remaining

那它就能判断当前状态是“值得立刻续跑”。

如果你完全不汇报,系统只能更保守地看待这次执行。


实战里最推荐的 cron 写法

一个健康 cron 更像这样:

  1. 先搜一小批待处理记录
  2. 逐条或逐小批处理
  3. 每批调用 _commit_progress(processed=n, remaining=rest)
  4. 如果返回剩余时间很少,就 break
  5. 下轮继续

核心不是“本轮必须清空”,而是:

  • 有推进
  • 可恢复
  • 不长时间独占 worker

这是 Odoo cron 最鼓励的节奏。


新手最常见的 4 个误区

误区 1:手动 commit() 就等于 _commit_progress()

不等于。

手动 commit() 只能提交事务,不能同步告诉 Odoo 还剩多少工作、这轮做了多少工作

误区 2:只要任务最终能跑完,跑多久都无所谓

错。

cron 是共享调度资源。你拖得越久,别的 job 越难及时跑到。

误区 3:报错就一定代表这次完全失败

也错。

如果前面已经成功提交了 progress,系统会把这次执行理解成“部分完成后失败”。这两种后果完全不同。

误区 4:remaining 不写也没关系

虽然可以不传,但只有 processed 不一定足够表达真实状态。

尤其当你的待处理集合会动态变化时,显式给出 remaining 会更稳。


一个很实用的判断标准

如果你的 cron 代码符合下面任一条,就该考虑改成 progress/batch 风格:

  • 单次可能处理上千条记录
  • 依赖外部 API,单条耗时不稳定
  • 容易在中间某条数据上报错
  • 需要和其他 cron 共用 worker 资源

基本上,大多数“夜间批处理”都应该这么写。


最后一句话

Odoo 的 cron 不是“后台偷偷跑完就好”的黑盒。

它真正的工作哲学是:

用可提交、可中断、可续跑的批处理,把长任务拆成调度器能消化的小块。

理解了 _commit_progress(),你写出来的 cron 才会像 Odoo 原生风格,而不是一段碰运气的后台脚本。

DISCUSSION

评论区

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