railsの起動プロセスを追う その3
はじめに
また間が空いてしまいましたが、 railsの起動までを見ていこう第3段ということで、今回は rails/all.rb
から見ていこうと思います。
以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。
※今回書いていて今更気がついたのですが、この起動プロセスについてはrails公式のドキュメントがありました。詳しくはこちらをご覧下さい。
rails.rbの読み込みと、app_classへの値のセット
以下が前回最後に読んだコードになります。
rails/all.rb at v5.2.1 · rails/rails · GitHub
# frozen_string_literal: true require "rails" %w( active_record/railtie active_storage/engine action_controller/railtie action_view/railtie action_mailer/railtie active_job/railtie action_cable/engine rails/test_unit/railtie sprockets/railtie ).each do |railtie| begin require railtie rescue LoadError end end
ここの最初で require "rails"
で読み込んでいるファイルは railties/lib/rails.rb
になります。
rails.rbで起動に関わる重要な部分は def application
の部分です。
rails/rails.rb at v5.2.1 · rails/rails · GitHub
def application @application ||= (app_class.instance if app_class) end
この application
の実体は app_class
のインスタンスですが、これは Rails::Application
を継承した時点でセットされます。
rails/application.rb at v5.2.1 · rails/rails · GitHub
def inherited(base) super Rails.app_class = base add_lib_to_load_path!(find_root(base.called_from)) ActiveSupport.run_load_hooks(:before_configuration, base) end
つまり、その2で紹介した ./config/application.rb
の下記部分が読み込まれた時点でセットされます。
上記のdef inherited
の base
には下記の Applicaiton
が渡ってくる事になります。
module TestRails class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.2
その後 all.rb
では各モジュールのrailtieをrequireしていますが、これ自体は各モジュールの初期化の意味合いが強いので、今回は飛ばします(sprockets/railtieは飛ばしていいものか悩みましたが、話がややこしくなるだけなので今回は一旦飛ばします)
再びperform
ここまでで、サーバを起動する為の準備が完全に整ったので、再びperformに戻っていきます。 rails/server_command.rb at v5.2.1 · rails/rails · GitHub
def perform set_application_directory! prepare_restart Rails::Server.new(server_options).tap do |server| # Require application after server sets environment to propagate # the --environment option. require APP_PATH Dir.chdir(Rails.application.root) server.start end end
前段までの話で、require APP_PATH
が実行されたときに何が起こるのかについては理解することが出来ました。
Dir.chdir(Rails.application.root)
は ./config/application.rb
内の Application
が持つ root
にカレントディレクトリを移すというのをやっています。
(おそらくですが、server.startする際にカレントディレクトリが重要になる局面があるのかなと勝手に推測しています)
実体は下記になります。
rails/rails.rb at v5.2.1 · rails/rails · GitHub
# Returns a Pathname object of the current Rails project, # otherwise it returns +nil+ if there is no project: # # Rails.root # # => #<Pathname:/Users/someuser/some/path/project> def root application && application.config.root end
ここでやっと、サーバ起動の本丸である server.start
に入っていきます。
serverの起動
server.start
の実体は、Rails::Server
クラスのインスタンスメソッド、start
です。
rails/server_command.rb at v5.2.1 · rails/rails · GitHub
def start print_boot_information trap(:INT) { exit } create_tmp_directories setup_dev_caching log_to_stdout if options[:log_stdout] super ensure # The '-h' option calls exit before @options is set. # If we call 'options' with it unset, we get double help banners. puts "Exiting" unless @options && options[:daemonize] end
trap(:INT) { exit }
は INT= SIGINTをアプリケーションが受け取ったときに、ブロック内のコールバックである exit を実行するという物です。
SIGINTというのは割り込みの事で、Ctrl-CとかをするとOSから発生するシグナルの事です。
その他の部分はメソッド名を見れば何しているかが大体分かるので割愛します。
このメソッドは super をしていますが、 そもそも Rails::Server
クラスは Rack::Server
を継承している物なため、このsuperの先は Rackになります。
Rackのstart
Rack側のstartメソッドは以下になります。
rack/server.rb at 2.0.5 · rack/rack · GitHub
def start &blk if options[:warn] $-w = true end if includes = options[:include] $LOAD_PATH.unshift(*includes) end if library = options[:require] require library end if options[:debug] $DEBUG = true require 'pp' p options[:server] pp wrapped_app pp app end check_pid! if options[:pid] # Touch the wrapped app, so that the config.ru is loaded before # daemonization (i.e. before chdir, etc). wrapped_app daemonize_app if options[:daemonize] write_pid if options[:pid] trap(:INT) do if server.respond_to?(:shutdown) server.shutdown else exit end end server.run wrapped_app, options, &blk end
wrapped_app
の部分より前は基本的にオプションの制御なので読み飛ばして問題ないです。
wrapped_app
は下記のように定義されています。重要なのはここで build_app
に渡している app
です。
このappは最終的に起動するサーバに渡される railsアプリケーション自身になります。
rack/server.rb at 2.0.5 · rack/rack · GitHub
def wrapped_app @wrapped_app ||= build_app app end
appの行方
appの定義は Rack::Server
を継承している Rails::Server
側でオーバーライドされているので、一旦そちらの定義を見てみます。
rails/server_command.rb at v5.2.1 · rails/rails · GitHub
def app @app ||= begin app = super if app.is_a?(Class) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0. Please change `run #{app}` to `run Rails.application` in config.ru. MSG end app.respond_to?(:to_app) ? app.to_app : app end end
ぱっと見ると、 app = super
で継承元の def app
の結果を app
に入れているように見えます。
といことで、継承元である Rack::Server
を見てみると以下のような形になっています。
rack/server.rb at 2.0.5 · rack/rack · GitHub
def app @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end
builderオプションが立っている場合とそうでない場合とで動作が違いますが、今回は立っていない方 = build_app_and_options_from_config
が呼ばれるパターンであると仮定します(これはrackの設定ファイルであるconfig.ruをrailsが持っている為で、build_app_and_options_from_config
は config.ru
から設定を読み込む形になっているからです)
build_app_and_options_from_config
の定義は以下のようになっています。
rack/server.rb at 2.0.5 · rack/rack · GitHub
def build_app_and_options_from_config if !::File.exist? options[:config] abort "configuration #{options[:config]} not found" end app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) @options.merge!(options) { |key, old, new| old } app end
重要なのは app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
の部分です。このメソッドが返す app
が最終的に起動するサーバに渡される railsアプリケーション自身になります。
Rack::Builder.parse_file
は以下で定義されています。
rack/builder.rb at 2.0.5 · rack/rack · GitHub
def self.parse_file(config, opts = Server::Options.new) options = {} if config =~ /\.ru$/ cfgfile = ::File.read(config) if cfgfile[/^#\\(.*)/] && opts options = opts.parse! $1.split(/\s+/) end cfgfile.sub!(/^__END__\n.*\Z/m, '') app = new_from_string cfgfile, config else require config app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join('')) end return app, options end
railsの場合、ここで引数のconfigに渡ってくるのは、基本的に作成したアプリケーションのrootディレクトリにある config.ru
です。
なので、if config =~ /\.ru$/
の中の処理が実行されます。
この処理の中では cfgfile = ::File.read(config)
で config.ru
の中身を読み込んで、cfgfile.sub!(/^__END__\n.*\Z/m, '')
で最終行の改行を消した後に
app = new_from_string cfgfile, config
を呼んで app
を取得しています。
eval
new_from_string
は第一引数の内容を Rack::Builder.new
のブロックに渡した上で、to_app
メソッドを呼ぶ事でappを取得するメソッドです。
rack/builder.rb at 2.0.5 · rack/rack · GitHub
def self.new_from_string(builder_script, file="(rackup)") eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", TOPLEVEL_BINDING, file, 0 end
なかなかトリッキーなことをしています。
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
は一旦後回しにして、evalの第2〜4の引数の内容を見ていきます。
第2引数の TOPLEVEL_BINDING
は evalの評価を TOPLEVEL_BINDING
で示したコンテキストで実行するという物です。これを指定しないと現在のコンテキストでevalが評価されるので、意図的にこれを指定している物と思われます。
第3引数のfileはスタックトレースに使われるファイル名で、今回は config.ru
です。
第4引数の0は評価される "Rack::Builder.new {\n" + builder_script + "\n}.to_app"
が何行目に書かれていると仮定するかという数字で、指定するとスタックトレース(だけではないですが)で以下のように表示が変わります。
irb(main):001:0> eval "raise",TOPLEVEL_BINDING, 'hoge.rb', 0 RuntimeError: from hoge.rb:in `<main>' from (irb):2:in `eval' from (irb):2 from /Users/hoge/.rbenv/versions/2.3.1/bin/irb:11:in `<main>' irb(main):002:0> eval "raise",TOPLEVEL_BINDING, 'hoge.rb', 5 RuntimeError: from hoge.rb:5:in `<main>' from (irb):3:in `eval' from (irb):3 from /Users/hoge/.rbenv/versions/2.3.1/bin/irb:11:in `<main>' irb(main):003:0>
ここまでで、evalの第2〜4の引数が何の意味を持つかが分かりました。
次は後回しにしていた eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
の中身を見ていきます。
evalで評価する物
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
では builder_script
を Rack::Builder.new
のブロックに展開した上で、to_app
した内容を最終的にメソッドの戻り値としています。
builder_script
は config.ru
の内容その物です。
ここまで config.ru
の内容をスルーしてきましたが、具体的なイメージがないとコードを追うのが難しくなってきたので、rails new
で新しく作成したrailsアプリケーションの config.ru
を見てみます。
# This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application
上記より、 Rack::Builder.new {\n" + builder_script + "\n}.to_app"
は以下と等価になります。
Rack::Builder.new { # This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application }.to_app
Rack::Builder
Rack::Builderではブロックで渡された内容を評価します。 rack/builder.rb at 2.0.5 · rack/rack · GitHub
def initialize(default_app = nil, &block) @use, @map, @run, @warmup = [], nil, default_app, nil instance_eval(&block) if block_given? end
instance_eval
の部分はDSLを解析する際によく用いられる書き方です。今回の場合だと config.ru
のDSLを解析する為に、 Rack::Builder
クラスに対してブロックで渡された物を評価させるというやり方を取っています。
config.ruの解析
config.ru
の解析をするところを読んでいくにあたり、先程示したconfig.ru
を例に処理の流れを見て行くことにします。
# This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application
require_relative 'config/environment'
で読み込んでいるファイルもrails.new
して作られる物です。内容は以下のような形になっています。
# Load the Rails application. require_relative 'application' # Initialize the Rails application. Rails.application.initialize!
require_relative 'application'
で読み込んでいる application
は「rails.rbの読み込みと、app_classへの値のセット」で説明した application.rbですので説明は省きます。
次のRails.application.initialize!
の中身は以下の通りです。
rails/application.rb at v5.2.1 · rails/rails · GitHub
# 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ですが、これは Applicaton が継承しているEngineクラスが継承しているRailtieがincludeしているInitializableモジュールのメソッドです。 rails/application.rb at v5.2.1 · rails/rails · GitHub
class Application < Engine
rails/engine.rb at v5.2.1 · rails/rails · GitHub
class Engine < Railtie
rails/railtie.rb at v5.2.1 · rails/rails · GitHub
class Railtie autoload :Configuration, "rails/railtie/configuration" include Initializable
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
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
処理を見ていくと、run_initializers
は一回以上実行されないようにクラスインスタンス変数 @run
を持っています。
最初はこれが定義されていないので、initializers
に対してtsort
した物を順にrunしていく形をしています。
initializers
の定義は以下の通りです。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers @initializers ||= self.class.initializers_for(self) end
initializers_for
の処理の詳細は後でもう一度追うのですが、今はinitializers_chain
をmapしているところだけを掘っていきます。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers_for(binding) Collection.new(initializers_chain.map { |i| i.bind(binding) }) end
initializers_chain
では、自身の継承チェーンを一番古い祖先から initializers
特異メソッドがあるかどうか = Initializable
が使えるクラスなのかどうなのかを判定し、
ある場合は 各クラスのクラスインスタンス変数 initializers
が定義されていればそれを、なければ空のCollection
クラスのインスタンスを Collection
クラスのインスタンスに追加していくということをしています。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers_chain initializers = Collection.new ancestors.reverse_each do |klass| next unless klass.respond_to?(:initializers) initializers = initializers + klass.initializers end initializers end
どういうことかと言うと、例えば、engine.rb
は以下のように initializer
クラスマクロを使った初期化処理の登録を行っています。
rails/engine.rb at v5.2.1 · rails/rails · GitHub
initializer :load_config_initializers do config.paths["config/initializers"].existent.sort.each do |initializer| load_config_initializer(initializer) end end
この initializer
クラスマクロはクラスインスタンス変数である initializers
に Initializer
クラスのインスタンスを追加しています。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializer(name, opts = {}, &blk) raise ArgumentError, "A block must be passed when defining an initializer" unless blk opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] } initializers << Initializer.new(name, nil, opts, &blk) end
クラスインスタンス変数である initializers
は特異メソッドである initializers
が呼ばれた際に値が入っていればその値をそのまま使われ、そうでなければ空のCollectionを返します。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers @initializers ||= Collection.new end
話を initializers_chain
を呼んでいる initializers_for
メソッドに戻します。
このメソッドで追わないといけない部分はi.bind(binding)
の部分です。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def initializers_for(binding) Collection.new(initializers_chain.map { |i| i.bind(binding) }) end
この処理を読む上で重要なのは i
が Initializer
クラスのインスタンスであるということです。
Initializer
クラスはインスタンスメソッドとして、bind
を持っているので、下記の処理が呼ばれることになります。
@context
は特異メソッドである initializer
がInitializer
クラスのインスタンスを作成した際はnilが渡るので、基本的にはbindメソッドに渡されたcontextに沿ってInitializer
クラスのインスタンスが作り直されます。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def bind(context) return self if @context Initializer.new(@name, context, @options, &block) end
ここまで読むと、run_initializers
まで戻ってこれます。
tsort_each
は rubyのトポジカルソートのライブラリです(今回はちゃんと調べる気がないので、一旦飛ばします。ごめんなさい)
initializer.belongs_to?(group)
はとりあえず必ず true
になる(groupにはdefaultしか渡ってこない上に、今回の場合だとinitializerのgroupがdefault以外になることはないと思われる)ので、initializer.run(*args)
が実行されます。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
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
initializer.run(*args)
の定義は下記になります。@context
はこの段階では self
です。
この処理でinitializer
クラスマクロでブロックに渡された初期化処理の内容が実行されます。
rails/initializable.rb at v5.2.1 · rails/rails · GitHub
def run(*args) @context.instance_exec(*args, &block) end
これでやっとconfig.ru
の解析をする部分まで戻ってきました。
# This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application
だいぶ長くなってしまったので、次回は run Rails.application
の部分から読んでいきます。
まとめ
- サーバを起動する直前で
config/application.rb
の内容を読み込んで各モジュールの初期化及び、@application
に自分自身を登録している。 - サーバの起動では、SIGINTをトラップ出来るようにしている。
- サーバの起動のほとんどは Rackの
start
メソッドが担当している。 - 'config/environment.rb' を読み込んだタイミングで、
initializer
クラスマクロで定義された初期化を行っている。