`rails s`読んだ
rails sでRailsサーバーが起動するまでに何が起きているのかを紐解くことでRailsとは何なのかを理解していきたい。今回読んでいくソースコードのコミットは2d9b9fb5b5f6015e66d3ad5cb96bc1ba117fd626だ。
TL;DR
bin/rails sがユーザーによって実行される。
コマンドの実行
まずbin/railsを見る。bin/railsはrails newを実行したときに生成されるのだが、このひな形はrailties/lib/rails/generators/rails/app/templates/bin/railsにある。
# bin/rails #!/usr/bin/env ruby APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands'
config/boot.rbとrails/commands.rbを見る。
# config/boot.rb ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' # Set up gems listed in the Gemfile.
- config/boot.rbはGemfileにあるgemをrequireするようだ。
# railties/lib/rails/commands.rb ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", "r" => "runner" } command = ARGV.shift command = aliases[command] || command require 'rails/commands/commands_tasks' Rails::CommandsTasks.new(ARGV).run_command!(command)
rails sと実行するとaliasesの中から"server"という文字列を取得してrails serverを実行することになる。
rails/commands/commands_tasksを見る。
# railsties/lib/rails/commands/commands_tasks.rb module Rails class CommandsTasks # ... def initialize(argv) @argv = argv end def run_command!(command) command = parse_command(command) if COMMAND_WHITELIST.include?(command) send(command) else write_error_message(command) end end # ... end end
#parse_commandは--versionや--helpをそれぞれ"version"と"help"というコマンドに変換するもの。それ以外はそのまま返す。COMMAND_WHITELISTに含まれていれば実行、そうでなければエラーを出力する。- 今回は
"server"がcommandに入るのでsend("server")が実行され、#serverが実行されることになる。
# railsties/lib/rails/commands/commands_tasks.rb module Rails class CommandsTasks # ... def server set_application_directory! require_command!("server") Rails::Server.new.tap do |server| require APP_PATH Dir.chdir(Rails.application.root) server.start end end # ... end end
#set_application_directory!はconfig.ruがないディレクトリからでもrails sを実行できるようにするためのものらしい。APP_PATHbin/railsの中で代入されたconfig/application.rbなので、require "config/application"をserver.startの前に実行している。
設定の読み込み
# config/application.rb require File.expand_path('../boot', __FILE__) require 'rails/all' Bundler.require(*Rails.groups) module SampleApp class Application < Rails::Application config.active_record.raise_in_transactional_callbacks = true end end
- 前述の通り、config/boot.rbは
require "bundler/setup"を実行しておりGemfile.lockに記載されたバージョンのrubygemをrequireしている。
rails/all.rbを見る。
# railties/lib/rails/all.rb require "rails" %w( active_record action_controller action_view action_mailer active_job rails/test_unit sprockets ).each do |framework| begin require "#{framework}/railtie" rescue LoadError end end
- Railsを構成する各rubygemのrailtieを
requireしている。
rails.rbを見る。
# railsties/lib/rails.rb module Rails # ... class << self # ... end end
- ここには
Rails.application,Rails.configuration,Rails.envなどの重要なメソッドが定義されているため、登場次第また見ていくことにする。
rails/all.rbとrails.rbについて見たので、config/applicationに戻る。
# config/application.rb # ... Bundler.require(*Rails.groups) module SampleApp class Application < Rails::Application config.active_record.raise_in_transactional_callbacks = true end end
Rails.groupsは上述したrails.rbで定義されているのでさっそく見る。
# railties/lib/rails.rb module Rails class << self # ... def env @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development") end # ... def groups(*groups) hash = groups.extract_options! env = Rails.env groups.unshift(:default, env) groups.concat ENV["RAILS_GROUPS"].to_s.split(",") groups.concat hash.map { |k, v| k if v.map(&:to_s).include?(env) } groups.compact! groups.uniq! groups end # ... end end
Rails.groupsはRails.envの値に合わせてBundlerが読みこむべきgroupを返す。Rails.envは環境変数"RAILS_ENV"または"RACK_ENV"から実行環境を返す。
config/application.rbに戻る。
# config/application.rb # ... module SampleApp class Application < Rails::Application config.active_record.raise_in_transactional_callbacks = true end end
SampleApp::ApplicationがRails::Applicationを継承するとき、以下のような実装によってRails::Application.inheritedが呼ばれ、Rails.app_classがSampleApp::Applicationとなる。
# railties/lib/rails/application.rb module Rails class Application < Engine class << self def inherited(base) super Rails.app_class = base add_lib_to_load_path!(find_root(base.called_from)) end end end end
サーバーの起動
サーバー起動前にどういった設定を読み込んでいるか見たので、サーバーの起動について詳細に見ていく。
# railsties/lib/rails/commands/commands_tasks.rb module Rails class CommandsTasks # ... def server set_application_directory! require_command!("server") Rails::Server.new.tap do |server| require APP_PATH Dir.chdir(Rails.application.root) server.start end end # ... end end
#require_command!("server")でrequire "rails/commands/server"をしている。
Rails::Serverはrails/commands/server.rbで定義されているので見る。
# railsties/lib/rails/commands/server module Rails class Server < ::Rack::Server # ... def initialize(*) super set_environment end # ... def start print_boot_information trap(:INT) { exit } create_tmp_directories log_to_stdout if options[:log_stdout] super ensure puts 'Exiting' unless @options && options[:daemonize] end # ... end end
- スーパークラスの
::Rack::Serverがサーバー起動において主な役割を果たしているようだ。 - これ以降はRackのソースコードを追うことになるが本題から反れるので、結論だけ言うとconfig.ruが実行されることになる。
config.ruを見る。
# config.ru require ::File.expand_path('../config/environment', __FILE__) run Rails.application
- config/environment.rbを読み込んでいる。
- その後
Rails.applicationをrackアプリケーションとして実行している。
とりあえずconfig/environment.rbを見る。
# config/environment.rb require File.expand_path('../application', __FILE__) Rails.application.initialize!
- config/application.rbは既に読み込まれているはず。
Rails.application.initialize!について見ていくため、まずはRails.applicationの定義を見る。
# railties/lib/rails.rb module Rails class << self def application @application ||= (app_class.instance if app_class) end end end
app_classは、config/application.rbでRails::Applicationのサブクラスが定義されたときにそのサブクラスが代入される。SampleApp::Application.instanceが呼ばれているが、これのメソッドはRails::Applicationに定義されていると思われる。
Rails::Applicationを見る。
# railties/lib/rails/application.rb module Rails class Application < Engine class << self def instance super.run_load_hooks! end end def run_load_hooks! return self if @ran_load_hooks @ran_load_hooks = true ActiveSupport.run_load_hooks(:before_configuration, self) @initial_variable_values.each do |variable_name, value| if INITIAL_VARIABLES.include?(variable_name) instance_variable_set("@#{variable_name}", value) end end instance_eval(&@block) if @block self end end end
SampleApp::Application.instance内でsuper.run_load_hooks!が呼ばれている。このsuperはスーパークラスで定義された.instanceを呼んでおり、スーパークラスをたどるとRails::Railtie.instanceが呼ばれていることがわかる。これはそのままnewを呼んでインスタンスを返すだけだ。なので、super.run_load_hooks!というのはSampleApp::Application#run_load_hooks!を指す。SampleApp::Application#run_load_hooks!はRails::Applicationで上のように定義されており自分自身を返す。中でActiveSupport.run_load_hooks(:before_configuration, self)を呼んでおり、これによって:before_configurationをフックとして登録しておいた処理が実行される。- 結局、
Rails.applicationはSampleApp::Applicationインスタンスということになる。
initializerの実行
config/environment.rbに戻る。
# config/environment.rb require File.expand_path('../application', __FILE__) Rails.application.initialize!
Rails.application.initialize!はつまりSampleApp::Application#initialize!ということ。
Rails::Applicationを見る。
# railties/lib/rails/application.rb module Rails class Application def initialize!(group=:default) #:nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true self end end end
#run_initializersはRails::Initializableで以下のように定義されている。
# railties/lib/rails/initializable.rb module Rails module Initializable 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 end end
#initializersはRails::Applicationで以下のように定義されている。
# railties/lib/rails/application.rb module Rails class Application def initializers Bootstrap.initializers_for(self) + railties_initializers(super) + Finisher.initializers_for(self) end end end
Bootstrap.initializers_forとかFinisher.initializers_forはRails::Initializableモジュールで以下のように定義されている。
# railties/lib/rails/initializable.rb def initializers_for(binding) Collection.new(initializers_chain.map { |i| i.bind(binding) }) end
Initializable::Collectionを初期化しているようだ。
# railties/lib/rails/initializable.rb class Collection < Array include TSort alias :tsort_each_node :each def tsort_each_child(initializer, &block) select { |i| i.before == initializer.name || i.name == initializer.after }.each(&block) end def +(other) Collection.new(to_a + other.to_a) end end
TSortモジュールはRubyの標準モジュールで、依存関係を解決する順番に並び替える(=トポロジカルソート)実装を提供する。#tsort_each_nodeと#tsort_each_childの2つを実装する必要がある。#tsort_each_nodeはすべての要素を走査するメソッド、#tsort_each_childは子要素を走査するメソッド。Collection#tsort_each_childでは与えられたinitializerの前後のinitlaizerに対してブロックを実行する。
Initializable.initializers_forに戻る。
# railties/lib/rails/initializable.rb def initializers_chain initializers = Collection.new ancestors.reverse_each do |klass| next unless klass.respond_to?(:initializers) initializers = initializers + klass.initializers end initializers end def initializers_for(binding) Collection.new(initializers_chain.map { |i| i.bind(binding) }) end
- 続いて
.initializers_chainを見る。 .ancestorsはスーパークラスやincludeしているモジュールを直接の親から順に並べて配列で返す。.ancestors.reverse_eachなので、最も遠いクラスまたはモジュールから順にinitializersを取得して一つのCollectionに連結させている。
initializers_chainの要素はおそらくInitializerインスタンスだと思われるので、Initializer#bindを見る。
# railties/lib/rails/Initializable.rb class Initializer def bind(context) return self if @context Initializer.new(@name, context, @options, &block) end end
Initializer#initializeの第2引数は、Initializer#runでinstance_execのレシーバとして利用される。今回の場合、これはRails::Applicationインスタンスとなる。
Rails::Application#initializersに戻る。
# railties/lib/rails/application.rb module Rails class Application def initializers Bootstrap.initializers_for(self) + railties_initializers(super) + Finisher.initializers_for(self) end end end
#railties_initializersがまだ残っているので見る。
# railties/lib/rails/application.rb module Rails class Application def ordered_railties @ordered_railties ||= begin order = config.railties_order.map do |railtie| # ... end all = (railties - order) all.push(self) unless (all + order).include?(self) order.push(:all) unless order.include?(:all) index = order.index(:all) order[index] = all order end end def railties_initializers(current) initializers = [] ordered_railties.reverse.flatten.each do |r| if r == self initializers += current else initializers += r.initializers end end initializers end end end
config.railties_orderはデフォルトでは[:all]を返すようになっているが、ここを変更することで実行するRailtieのinitializerの順番を変更することもできるようだ。#ordered_railtiesが返すのはall = (railties - order)の部分なので、あとで詳しく#railtiesについて見る。- ある順番でソートされた各Railtieの
initializersを結合して返している。
#railtiesはRails::Engineから継承されたメソッドなので見る。
# railties/lib/rails/engine.rb def railties @railties ||= Railties.new end
Rails::Engine::Railtiesを見る。
# railties/lib/rails/engine/railties.rb def initialize @_all ||= ::Rails::Railtie.subclasses.map(&:instance) + ::Rails::Engine.subclasses.map(&:instance) end
::Rails::RailtieまたはRails::Engineのサブクラスをすべて返している(!)
railties_initializersに戻る。
# railties/lib/rails/application.rb def railties_initializers(current) initializers = [] ordered_railties.reverse.flatten.each do |r| if r == self initializers += current else initializers += r.initializers end end initializers end
ordered_railtiesは::Rails::Railtieまたは::Rails::Engineのサブクラスすべてだということがわかった。- よって、それらの
initializersをすべて連結したものを返していることになる。
Rails::Application#initializersについて見たので、#run_initializersに戻る。
# railties/lib/rails/initializable.rb 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はInitializable::Collectionインスタンスなので、#tsort_eachによって依存関係を解決する順番で#eachを行う。
Initializer#runを見る。
# railties/lib/rails/initializable.rb class Initializer def run(*args) @context.instance_exec(*args, &block) end end
@contextは#bindによってセットされる。blockはInitializerが初期化される際に渡されたブロックだ。このblockは#bindによってセットされた@contextをレシーバとして実行される。- 今回の場合、
@contextはRails::Applicationインスタンスをレシーバとしてblockが実行されることになる。
続いて、実行されるInitializerがどのように初期化されて登録されているのか見ていく。これはRails::Initializable::ClassMethods.initializerによって行われる。
# railties/lib/rails/initializable.rb 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
opts[:after]はInitializerインスタンス間の依存関係の解決に利用される。initializerで特に指定しなければ既存のinitializersの最後の要素がinitializer#afterになる。
Railsアプリケーションの起動
initializersの実行について一通り眺めたのでこれでconfig/environment.rbを読んだことになる。config.ruに戻る。
# config.ru require ::File.expand_path('../config/environment', __FILE__) run Rails.application
- ようやく
run Rails.applicationでアプリケーションを起動できる。 Rails.applicationがSampleApp::Applicationインスタンスであることは上述の通り。