【问题标题】:Rails 4.2 Autoloading not thread-safeRails 4.2自动加载不是线程安全的
【发布时间】:2015-04-03 20:35:40
【问题描述】:

我有以下型号:

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 模型可以很简单,例如app/models/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,但问题似乎仍然存在。 我还能尝试什么来缓解这个问题?

【问题讨论】:

  • 在自动加载之前尝试手动要求它
  • 您能详细说明一下吗?就像在线程开始之前调用 MessagePoro.new 一样?万一这正在修复它,我仍然对潜在问题感到好奇
  • 没有。目前你有基本的 Rails 自动加载设置。如果您首先在代码中的某处调用 MessagePoro,它将根据某些约定要求它(例如 MyModule::MessagePoro 应该在 autoload_path/my_module/message_poro.rb 中)。但您可以尝试手动要求它 require 'path/to/message_poro'。
  • 你使用的是什么 ruby​​ 版本?
  • 我在使用 eager_loading、Ruby 2.1 和 Rails 4.1 和 4.2 时遇到了同样的问题

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


【解决方案1】:

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

TL;DR:

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

  1. 您必须在 Rails 配置中设置 config.eager_load = true(在生产环境中默认设置)
  2. 你的类要急切加载的文件必须在config.eager_load_paths,而不是config.autoload_paths

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

更多信息

正如 digidigo 在他的回复中提到的,循环依赖错误来自 ActiveSupport::Dependencies 模块,或者更笼统地说是 Rails 自动加载器。此代码不是线程安全的,因为它使用该类/模块变量来存储它正在加载的文件。如果两个线程最终同时自动加载相同的东西,其中一个线程可能会因为在该类变量中看到要加载的文件并引发“循环依赖”错误而被误导。

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

事实证明,向autoload_pathsautoload_once_paths 添加一些东西NOT 也意味着它会被急切加载所拾取。然而,情况恰恰相反——添加到 eager_load_paths 的路径将被视为自动加载,如果 eager_load 被禁用(请参阅 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 的自动加载器似乎绝对不是线程安全的。

【讨论】:

  • 我相信我必须在 Rails 中尝试自动加载控制器来处理这个线程问题。除了在开发中急切加载之外,我如何在 Api::V1 中为控制器 require 或 require_dependency?请阅读这篇文章:stackoverflow.com/questions/62927007/…
【解决方案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 

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

# 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 不是线程安全的?

【讨论】:

    【解决方案3】:

    经过一些研究,事实证明,自动加载现在是线程安全的。所以这可能是一种回归。结帐Threading with the AWS SDK for Ruby。该补丁由 Charles Nutter 在 ruby​​ 2.0.0 autoload is not thread-safe987654322@

    中引入

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

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

    【讨论】:

    • 感谢您的意见。我了解您的解决方案可以治愈症状,但我觉得这并不能解决根本问题
    • 它实际上是因为它转储自动加载:),这是根本问题
    • 请发布您的 ruby​​ 版本并让我们向 Rails 或 ruby​​ 提交错误请求。如果您可以创建一个 Demo github Repo 来消除该错误,那就太好了
    • ActiveSupport 抛出循环依赖错误,不是 ruby​​ 问题。在我看来它仍然不是线程安全的。
    猜你喜欢
    • 2011-02-19
    • 1970-01-01
    • 2017-10-24
    • 1970-01-01
    • 2012-10-24
    • 2010-10-13
    • 1970-01-01
    • 1970-01-01
    • 2012-05-11
    相关资源
    最近更新 更多