Rails 3多线程运行Rake任务偶发uninitialized constant错误如何解决?
问题根因
- Rails 3的常量自动加载机制是非线程安全的:Rails 3默认使用ActiveSupport的延迟自动加载逻辑,当代码第一次引用未加载的常量时会触发
const_missing回调加载对应文件。多线程环境下多个线程同时触发同一个常量的加载逻辑时会出现竞态,直接导致偶发的uninitialized constant错误,和你描述的偶发、无法稳定复现的特征完全匹配。 - 工作线程未继承主线程预加载的常量:你的自定义线程池的工作线程是独立初始化的,默认不会共享主线程已经加载的所有常量上下文,如果常量没有被主线程提前加载,就会在工作线程执行时触发自动加载,刚好踩到线程不安全的坑。
- Rake任务默认未开启全量预加载:Rails 3默认不会在Rake任务启动时预加载所有业务类,类似
NotificationService这种自定义服务类如果没有被主线程提前引用过,就会延迟到工作线程执行时才加载。
解决方案
优先推荐前置预加载的方案,从根源避免工作线程触发自动加载:
- 方案1:手动预加载业务用到的所有常量
在线程池初始化之前,主动引用所有会在工作线程中用到的类,触发主线程提前完成加载,示例:def perform # 新增:预加载所有线程内会用到的常量 NotificationService Notification Device # 其他自定义业务类都在这里提前引用一次 ReadReplicaHelper.read_from_slave do # 原有逻辑不变 end end - 方案2:开启全量Eager Loading
如果用到的业务类较多,直接在Rake任务开头触发全量预加载:
注意需要提前把自定义类的存放路径加到Rails配置的Rails.application.eager_load!config.eager_load_paths中。 - 方案3:自动加载阶段加锁(性能稍差,适合无法提前枚举所有类的场景)
定义全局互斥锁,保证常量加载过程串行执行,修改线程池的任务执行逻辑:# 全局互斥锁 CONST_LOAD_LOCK = Mutex.new # 线程池内执行任务的逻辑修改为 loop do job, args = @jobs.pop CONST_LOAD_LOCK.synchronize do job.call(*args) end end
额外注意事项
Rails 3的ActiveRecord数据库连接同样不是线程安全的,你当前的代码直接在工作线程中操作ActiveRecord很容易出现连接泄漏、连接复用错乱的问题,建议把工作线程的业务逻辑包在连接池获取块中:
pool.schedule do ActiveRecord::Base.connection_pool.with_connection do pool_method notification, last_status, before_last_status end end
内容的提问来源于stack exchange,提问作者user2274074
相关产品推荐
相关产品推荐

