Railsの自動読み込み eager_loadの処理を調べた

eager_loadがどのように行われているのか知りたいので調べた。 環境は

eager_loadの処理

Railsのサーバーを立ち上げ時に config.ru ファイルが読み込まれる。

# config.ru

# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

run Rails.application

config.ruはconfig/environment.rbを読み込む。

# config/environment.rb

# frozen_string_literal: true

# Load the Rails application.
require_relative 'application'

# Initialize the Rails application.
Rails.application.initialize!

config/environment.rbは config/application.rb を読み込み、アプリケーションの設定などを確保する。

その後 Rails.application.initialize! を実行する。 initialize! から処理がRailsの内部に入っていく。

# https://github.com/rails/rails/blob/v6.0.2/railties/lib/rails/application.rb#L359-L366

# Initialize the application passing the given group. By default, the
# group is :default
def initialize!(group = :default) #:nodoc:
  raise "Application has been already initialized." if @initialized
  run_initializers(group, self)
  @initialized = true
  self
end

実際の初期化処理は run_initializers が行っている。

# https://github.com/rails/rails/blob/v6.0.2/railties/lib/rails/initializable.rb#L58-L64

def run_initializers(group = :default, *args)
  return if instance_variable_defined?(:@ran)
  initializers.tsort_each do |initializer|
    initializer.run(*args) if initializer.belongs_to?(group)
  end
  @ran = true
end

最後にinitializeしたというフラグを @ran に格納している。すでに実行済みであれば再実行されないようになっている。

initializers.tsort_each のinitializersは Rails::Application#initializers を実行している。

※ run_initializersが定義されているRails::Initializeableにも #initializers メソッドはあるが、Rails::Application#initializersが実行されるのはメソッド探索時にRails::Application#initializersが先に見つかるから

# https://github.com/rails/rails/blob/v6.0.2/railties/lib/rails/application.rb#L368-L372

def initializers #:nodoc:
  Bootstrap.initializers_for(self) + railties_initializers(super) + Finisher.initializers_for(self)
end

eager_loadの処理はFinisherに定義されてる

# https://github.com/rails/rails/blob/v6.0.2/railties/lib/rails/application/finisher.rb#L116-L125

initializer :eager_load! do
  if config.eager_load
    ActiveSupport.run_load_hooks(:before_eager_load, self)
    # Checks defined?(Zeitwerk) instead of zeitwerk_enabled? because we
    # want to eager load any dependency managed by Zeitwerk regardless of
    # the autoloading mode of the application.
    Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk)
    config.eager_load_namespaces.each(&:eager_load!)
  end
end

config.eager_load は config/environments/development.rb とかで代入している config.eager_load の値が参照される。

まず ActiveSupport.run_load_hooks(:before_eager_load, self) でbefore_eager_loadという名前に登録されたブロックをselfをselfのコンテキストで実行する。

その後 Zeitwerk::Loader.eager_load_all が実行される(autoloaderがzeitwerkであろうと、clasicだろうと関係なくZeitwerk::Loader.eager_load_allが実行される必要があるらしいが、詳しくどういう理由なのはわからなかった。zeitwerk gemが必要としているのかな)。

config.eager_load_namespaces.each(&:eager_load!) ではeager_loadする名前空間をそれぞれeager_loadします。

[1] pry(main)> TestApp::Application
=> [I18n,
 ActiveSupport,
 ActionDispatch,
 ActiveModel,
 GlobalID,
 ActionView::Railtie,
 ActionView,
 ActionController,
 ActiveRecord,
 ActionMailer,
 ActionCable::Engine,
 ActionCable,
 Bootstrap::Rails::Engine,
 Devise::Engine,
 TestApp::Application]

eager_load! の実体は下記

# https://github.com/rails/rails/blob/v6.0.2/railties/lib/rails/engine.rb#L472-L483

def eager_load!
  # Already done by Zeitwerk::Loader.eager_load_all in the finisher.
  return if Rails.autoloaders.zeitwerk_enabled?

  config.eager_load_paths.each do |load_path|
    # Starts after load_path plus a slash, ends before ".rb".
    relname_range = (load_path.to_s.length + 1)...-3
    Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
      require_dependency file[relname_range]
    end
  end
end

zeitwerkを使っている場合はreturn、classicの場合はrequire_dependencyを使ってファイルをロードする。

参考

memo

require, require_dependencyの違い

require(path)

  • pathが絶対パスのときはそのパスのファイルを読み込む
  • 相対パスのときは $LOAD_PATH 内のパスを順番に探して最初に見つかったファイルをロードする
  • 拡張子は補完される
    • .rb, .so,.o,.dll など
    • .rbが補完される
  • 同じファイルを複数回読み込む

require_dependency(path)

  • rails(active_support)のメソッド
  • production環境ではrequire, development環境ではloadが実行される