見出し画像

Ruby on Railsのeager_load_pathsの仕組みを紐解く。

好きなフレームワークはFlask@teitei_tk です。

趣味でRuby on Railsというフレームワークのソースコードを読んでいるのですが、
eager_load_pathsと、autoload_pathsの違いを調べたくなりました。

軽く検索をしてみると、autoload_pathsの代わりにeager_load_paths使えばなんか動くからokと言われてるので、それはなぜなのかというのを実際にソースコードから追っていった記事です。

※自分の理解内のため、間違えがあるかもしれません。

---

autoload_pathsとは

語弊を恐れずに言うと、moduleや定数をいい感じに自動で読み込んでくれる仕組みです。

例えば


class Hoge < ApplicationRecord
end

のようなActiveRecordのclassを作るときに、Pure Rubyであれば 

require 'application_record


とapplication_recordを読まないと行けないと思いますが、それを自前で読み込まずともいい感じに自動読み込みしてくれる仕組みです。

詳しい仕組みはまとめられております。


eager_load_pathsとは


> config.eager_load_pathsは、パスの配列を引数に取ります。Railsは、cache_classesがオンの場合にこのパスから事前一括読み込み(eager load)します。デフォルトではアプリのappディレクトリ以下のすべてのディレクトリが対象です。

例えばドメイン知識が関係しないようなコードを  lib/other/hoge.rb に追加したとします。
この場合は上記のautoload_pathsの対象外になっています。なので読み込みはしておらず、NameErrorが発生します。

teitei.tk >> (master) ~/.golang/src/github.com/teitei-tk/dive-to-rails-autoloading
$ bin/rails c
Running via Spring preloader in process 10255
Loading development environment (Rails 5.2.1)
irb(main):001:0> ApplicationRecord
=> ApplicationRecord(abstract)
irb(main):002:0> Other
Traceback (most recent call last):
       1: from (irb):2
NameError (uninitialized constant Other)
irb(main):003:0>


module DiveToRailsAutoloading
 class Application < Rails::Application
   # Initialize configuration defaults for originally generated Rails version.
   config.load_defaults 5.2
   # Settings in config/environments/* take precedence over those specified here.
   # Application configuration can go into files in config/initializers
   # -- all .rb files in that directory are automatically loaded after loading
   # the framework and any gems in your application.
 end
end

Rails version5からの挙動の変更

Rails5からautoload_pathsに追加したファイルは RAILS_ENV=production では読み込まないようになってます。

https://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#autoloading-is-disabled-after-booting-in-the-production-environment


Autoloading is now disabled after booting in the production environment by default.
Eager loading the application is part of the boot process, so top-level constants are fine and are still autoloaded, no need to require their files.
Constants in deeper places only executed at runtime, like regular method bodies, are also fine because the file defining them will have been eager loaded while booting.
For the vast majority of applications this change needs no action. But in the very rare event that your application needs autoloading while running in production mode, set Rails.application.config.enable_dependency_loading to true.

今後はeager_load_pathsを使おうねという圧力を感じます。

では、実際にeager_load_pathsを利用した場合の流れを、Railsのコードから追っていきたいと思います。

Railsの起動の流れについて


Yasslabさんが日本語記事を書いてくださっています。


その中の、2 Railsを読み込むというところからやっていきます。


Rails.application.initialize!

config/environment.rbに、Rails.application.initialize!という処理が書いてあると思います。

# Load the Rails application.
require_relative 'application'

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

次のコードからRails本体のコードに入ります。

$ rails newで生成しているprojectでは、config/application.rbというところにファイルにアプリケーションの設定を書くと思います。
eager_load_pathsや、autoload_pathsなどはここによく書きますね。

module DiveToRailsAutoloading
 class Application < Rails::Application

実際にinitialize! methodの中を見ていきたいと思います。




   # 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というmethodを読んで、初期化のflagを立てています。

run_initializersは、Rails::Initializable というmoduleで定義されています。


みると、

   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

initializersというのを読んでいますね。

ここでまた railties/lib/rails/application.rb 戻ります。

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

ここが初期化の実態です。

Bootstrap.initializers_for

では起動時の初期化処理を、

railties_initializers

ではrailties(Railsのコアモジュール)の初期化処理を、

Finisher.initializers_for

でそれ以外の初期化処理を行っています。eager_loadの処理はFinisherに定義されています。

     initializer :eager_load! do
       if config.eager_load
         ActiveSupport.run_load_hooks(:before_eager_load, self)
         config.eager_load_namespaces.each(&:eager_load!)
       end
     end

それでは、実際にeager_load_pathsに追加してみます。


module DiveToRailsAutoloading
 class Application < Rails::Application
   # Initialize configuration defaults for originally generated Rails version.
   config.load_defaults 5.2
+    config.eager_load_paths += Dir["#{Rails.root}/lib"] 
   # Settings in config/environments/* take precedence over those specified here.
   # Application configuration can go into files in config/initializers
   # -- all .rb files in that directory are automatically loaded after loading
   # the framework and any gems in your application.
 end
end


teitei.tk >> !(master) ~/.golang/src/github.com/teitei-tk/dive-to-rails-autoloading
$ bin/rails c
Running via Spring preloader in process 11757
Loading development environment (Rails 5.2.1)
irb(main):001:0> Other
=> Other

名前解決ができるようになりました。

autoload_pathsに追加していないのに、なぜ名前解決ができるのか?

答えは railties/lib/rails/engine.rbに書いてありました。


   # Add configured load paths to Ruby's load path, and remove duplicate entries.
   initializer :set_load_path, before: :bootstrap_hook do
     _all_load_paths.reverse_each do |path|
       $LOAD_PATH.unshift(path) if File.directory?(path)
     end
     $LOAD_PATH.uniq!
   end

initializerを利用して、$LOAD_PATHにunshiftで追加をしていますね。
$LOAD_PATHとはRubyライブラリをロードするときの検索パスです。https://docs.ruby-lang.org/ja/latest/method/Kernel/v/=2dI.html

LOAD_PATHに Array#unshift で追加されている _all_load_pathsとはなんでしょうか。

実態は

config.autoload_paths +
config.eager_load_paths +
config.autoload_once_paths

をあわせたものでした。


     def _all_autoload_paths
       @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq
     end

     def _all_load_paths
       @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq
     end

config.eager_load_pathsも含まれていますね。
これにより、RAILS_ENV=production 以外では、autoload_pathsの仕組みを利用し、moduleがいい感じにloadされています。

そして、実際production環境では本当にfileが読まれているのかもみましたが、eager_load!時にちゃんと読み込んでいますね。


   # Eager load the application by loading all ruby
   # files inside eager_load paths.
   def eager_load!
     config.eager_load_paths.each do |load_path|
       matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
       Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
         require_dependency file.sub(matcher, '\1')
       end
     end
   end



     initializer :eager_load! do
       if config.eager_load
         ActiveSupport.run_load_hooks(:before_eager_load, self)
         config.eager_load_namespaces.each(&:eager_load!)
       end
     end

という流れでした。自分からわかったことは人類にRailsを使うことは早すぎたという事です。
現場からは以上です。


Software Engineer. https://teitei-tk.com