Rails 4.2自动加载不是线程安全的

时间:2015-04-03 20:35:41

标签: multithreading ruby-on-rails-4 eager-loading

我有以下型号:

class User < ActiveRecord::Base
  def send_message(content)
    MessagePoro.new(content).deliver!
  end

  def self.send_to_all(content)
    threads = []
    all.each do |user|
      threads << Thread.new do
        user.send_message(content)
      end
    end
    threads.each(&:join)
  end
end

MessagePoro模型可以是简单的,例如应用程序/模型/ message_poro.rb:

class MessagePoro
  def initialize(content)
    # ...
  end

  def deliver!
    # ...
  end
end

现在,当我有例如100个用户,我正在运行User.send_to_all(“test”)我有时得到更严重的错误:

RuntimeError: Circular dependency detected while autoloading constant MessagePoro

或:

wrong number of arguments (1 for 0)

我认为一定是因为没有加载MessagePoro而且所有线程都试图同时加载它,或类似的东西。由于这些错误有时只会发生,我很确定只有当存在“竞争条件”或者有线程时才会有这些错误。我已经尝试在启动Threads之前初始化MessagePoro,并且我已经使用了eager_loading,但问题似乎仍然存在。 还有什么可以尝试缓解这个问题?

3 个答案:

答案 0 :(得分:7)

我最近在尝试使用放置在[rails_root]/lib目录中的额外自定义库时遇到了一个非常类似的问题。

TL; DR:

您可以使用预先加载来解决问题,因为这可以确保在任何实际代码运行之前所有常量/模块/类都在内存中。但是为了这个工作:

  1. 您必须在Rails配置中设置config.eager_load = true(默认情况下,这在生产环境中已完成)
  2. 您要加载的课程所在的文件必须位于config.eager_load_paths,而不是config.autoload_paths
  3. OR

    您可以使用requirerequire_dependency(另一个ActiveSupport功能)来确保在Rails自动加载之前显式加载您需要的代码。

    更多信息

    正如digidigo在他的回复中提到的,循环依赖性错误来自ActiveSupport::Dependencies模块,或 Rails自动加载器更一般的术语。此代码 not threadsafe ,因为它使用该类/模块变量来存储正在加载的文件。如果两个线程最终同时自动加载同一个东西,那么其中一个线程可能会因为看到要在该类变量中加载的文件并引发“循环依赖”错误而误导。

    我在生产模式下使用(线程化的)Puma网络服务器运行Rails时遇到了这个问题。我们在Rails根目录的lib目录中添加了一个小型库,最初将lib添加到config.autoload_once_paths。在开发中一切都很好,但在生产中(启用了config.eager_loadconfig.cache_classes),偶尔我们会在几乎同时发出的请求中得到这些相同的循环依赖问题。经过几个小时的调试后,我最终看到了眼前的非线程安全问题,当围绕循环依赖关系逐步执行ActiveSupport代码并看到不同的线程在代码中的不同点处获取时。第一个线程将添加文件加载到loading数组中,然后第二个线程会在那里找到它并引发循环依赖性错误。

    事实证明,向autoload_pathsautoload_once_paths添加内容 NOT 也意味着它会被急切的加载所吸引。但事实恰恰相反 - 如果禁用eager_load,则会考虑添加到eager_load_paths的路径进行自动加载(有关详细信息,请参阅this article)。我们切换到eager_load_paths,到目前为止还没有进一步的问题。

    有趣的是,就在Rails 4测试版之前,默认情况下在生产环境中禁用了自动加载,这意味着像这样的问题会导致100%的时间出现硬故障,而不是奇怪的线程失败5%的时间。然而,这个版本在4.0测试版发布之前就已经恢复了 - 你可以看到一些(热情的)关于它的讨论here(包括选择短语'老实说,你告诉我去自己做什么?') 。从那时起,这个恢复已经在Rails 5.0.0beta1之前被恢复,所以希望将来再少有人能够解决这个问题的头痛。

    额外说明:

    Rails自动加载器完全独立于Ruby自动加载器 - 这似乎是因为Rails在尝试自动加载常量时对目录结构做了更多的推断。

    从Ruby 2.0开始,Ruby的自动加载似乎已成为线程安全,但这与Rails自动加载代码无关。如前所述,Rails的自动加载器似乎绝对是线程安全。

答案 1 :(得分:2)

这不是一个真正的答案,但我确实有更多的信息。抛出的错误来自ActiveSupport

 if file_path
    expanded = File.expand_path(file_path)
    expanded.sub!(/\.rb\z/, '')

    if loading.include?(expanded)
      raise "Circular dependency detected while autoloading constant #{qualified_name}"
    else
      require_or_load(expanded, qualified_name)
      raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false)
      return from_mod.const_get(const_name)
    end
  elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix)
    return mod
  elsif 

经过进一步研究,我们可以看到loading是一个类变量。

# Stack of files being loaded.
mattr_accessor :loading
self.loading = []

检查同一文件的两个线程:

第一个线程点击此代码并将路径放入加载

      loading << expanded

然后第二个线程检查扩展和命中

表示的路径
 if loading.include?(expanded)
      raise "Circular dependency detected while autoloading constant #{qualified_name}"

我错过了什么? ActiveSupport :: Dependencies不是线程安全的吗?

答案 2 :(得分:0)

经过一番研究后发现,自动加载现在是线程安全的。所以它可能是回归。结帐Threading with the AWS SDK for Ruby。该补丁由Charles Nutter在ruby 2.0.0 autoload is not thread-safe

中介绍

无论如何只有这个类,你可以通过手动要求来避免自动加载它。 只需手动使用它。

require 'message_poro'
class User
def self.send_to_all(content) 
  ... 
end