先说结论
Odoo 的 cron 设计目标,从来不是“这次触发后把所有数据一次跑完”。
真正的设计是:
每次跑一小批,明确提交进度,如果还没做完,就尽快安排下一轮继续。
所以你如果把 cron 写成一个超长大循环:
- 中间不提交进度
- 不告诉系统还剩多少
- 一直占着 worker 不放
那它看起来像“认真工作”,实际却是在和调度器对着干。
_commit_progress() 干的不是“记个日志”
ir_cron.py 里的 _commit_progress() 文档写得很直白:
processed:这一步处理了多少条remaining:还剩多少条deactivate:这次跑完后是否停用 cron
最关键的是它内部真的会:
- 写
ir.cron.progress self.env.cr.commit()- 返回当前这一轮 cron 还剩多少秒可以继续跑
也就是说,这不是一个“装饰性 API”,而是一个调度契约。
很多人误解成:
- 只是为了后台页面显示进度
- 不调用也没事
其实不对。
不调用它,系统就更难判断:
- 你到底有没有做成事
- 还要不要立刻续跑
- 这次异常算完全失败,还是“虽然报错,但前面已经推进了一部分”
为什么 Odoo 要强调“部分完成”
看 _run_job() 的逻辑,会发现 Odoo 并不把执行结果简单分成“成功 / 失败”。
它至少区分:
FULLY_DONEPARTIALLY_DONEFAILED
这个区别非常重要。
比如一个 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 更像这样:
- 先搜一小批待处理记录
- 逐条或逐小批处理
- 每批调用
_commit_progress(processed=n, remaining=rest) - 如果返回剩余时间很少,就
break - 下轮继续
核心不是“本轮必须清空”,而是:
- 有推进
- 可恢复
- 不长时间独占 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
评论区