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 では読み込まないようになってます。
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