Rails 4.2自动加载不支持多线程

10

我有以下模型:

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

消息Poro(Plain Old Ruby Object)模型可以是一些简单的东西,例如 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没有加载,所有线程都尝试同时加载它,或者类似于这样的情况。由于这些错误只会偶尔发生,我相信只有在存在“竞态条件”或与线程有关的情况下才会出现。我已经尝试在启动线程之前初始化MessagePoro,并尝试过渴望加载,但问题似乎仍然存在。 还有什么其他方法可以缓解这个问题?


尝试手动加载之前手动要求它。 - dre-hh
你能详细解释一下吗?比如在线程开始之前调用MessagePoro.new? 如果这样可以解决问题,我仍然对潜在的问题很好奇。 - Benedikt B
目前你已经有了基本的Rails自动加载设置。如果你在代码中的某个地方首先调用MessagePoro,它将根据某些约定进行要求(例如MyModule :: MessagePoro应该在autoload_path / my_module / message_poro.rb中)。但是你也可以尝试手动要求它require 'path/to/message_poro'。 - dre-hh
你使用的 Ruby 版本是什么? - dre-hh
我在使用 eager_loading、Ruby 2.1 和 Rails 4.1 和 4.2 时遇到了相同的问题。 - Benedikt B
3个回答

10

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

TL;DR:

您可以使用渴望加载(eager loading)来解决此问题,因为这可以确保在任何实际代码运行之前,所有常量/模块/类都在内存中。但是,要使其正常工作:

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

或者

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

更多信息

正如digidigo在回复中提到的那样,循环依赖错误来自于ActiveSupport::Dependencies模块或更一般的说是Rails自动加载器。该代码不是线程安全的,因为它使用该类/模块变量来存储正在加载的文件。如果两个线程同时自动加载相同的内容,则其中一个可能会被看到已经在该类变量中加载的文件所误导,并抛出“循环依赖”错误。

在使用(线程化的)Puma web服务器以生产模式运行Rails时,我遇到了这个问题。我们在Rails根目录下添加了一个小库到lib目录,并最初将lib添加到config.autoload_once_paths中。在开发环境中一切正常,但在生产环境中(启用了config.eager_loadconfig.cache_classes),偶尔会出现几乎同时请求的循环依赖问题。几个小时的调试后,当我在围观循环依赖周围的ActiveSupport代码并看到不同的线程在代码的不同点上开始执行时,我看到了非线程安全性发生的情况。第一个线程将要加载的文件添加到loading数组中,然后第二个线程找到它并引发了循环依赖错误。

事实证明,将某些内容添加到autoload_pathsautoload_once_paths中,并不意味着它也会被急切地加载。然而相反的情况是正确的——如果禁用了急切加载,则添加到eager_load_paths的路径将被考虑用于自动加载(有关详细信息,请参见此文章)。我们切换到eager_load_paths,并且到目前为止没有出现其他问题。

有趣的是,在 Rails 4 beta 版本发布之前,自动加载在生产环境中默认情况下被禁用,这意味着像这样的问题将会导致100%的硬失败,而不是5%的古怪线程问题。 然而,在发布4.0 beta版本之前它被撤消了 - 您可以在这里看到相关的(充满激情的)讨论(包括“老实说,你让我去***自己?”)。从那时起,该撤消已在Rails 5.0.0beta1之前再次撤消,因此希望将来更少的人不得不处理这个头疼的问题。

额外说明:

Rails的自动装载器与Ruby的自动装载器完全独立 - 这似乎是因为Rails在尝试自动加载常量时更多地推测目录结构。

据称自Ruby 2.0起,Ruby的自动加载已经成为线程安全的,但这与Rails的自动加载代码无关。如上所述,Rails的自动加载程序显然是线程安全的。


我相信我需要处理Rails中的线程问题,尝试自动加载控制器。除了在开发中使用急切加载外,我如何要求或require_dependency Api::V1中的控制器?请阅读此帖子:https://stackoverflow.com/questions/62927007/rails-autoload-issue-after-installing-sidekiq-argumenterror-a-copy-of-apiv1 - bigmugcup

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

然后第二个线程会检查由"expanded"表示的路径,并击中。
 if loading.include?(expanded)
      raise "Circular dependency detected while autoloading constant #{qualified_name}"

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

0

经过一些研究,发现自动加载现在是线程安全的。所以这可能是一个回归问题。请查看使用AWS SDK for Ruby进行线程处理。该补丁由Charles Nutter在ruby 2.0.0中引入自动加载不是线程安全的

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

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

谢谢您的输入。我知道您的修复解决了症状,但我觉得这并没有解决根本问题。 - Benedikt B
它实际上会卸载自动加载 :), 这是根本问题。 - dre-hh
请发布您的 Ruby 版本,让我们向 Rails 或 Ruby 提交 Bug 请求。如果您能创建一个演示 GitHub 存储库来重现该 Bug,那就太好了。 - dre-hh
1
循环依赖错误是由ActiveSupport引发的,而不是Ruby问题。而且在我看来,它仍然不是线程安全的。 - digidigo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接