本章は、Rails初期化プロセスの内部について解説します。上級Rails開発者向けに推奨される、きわめて高度な内容を扱っています。
このガイドの内容:
rails serverの使用法Rails::Serverインターフェイスの定義方法と利用法本章では、デフォルトのRailsアプリケーション向けにRuby on Railsスタックの起動時に必要となるすべてのメソッド呼び出しについて詳細に解説します。具体的には、rails serverを実行してアプリケーションを起動したときにどのようなことが行われているかに注目して解説します。
文中に記載されるRuby on Railsアプリケーションへのパスは、特に記載のない限り相対パスを使用します。
Railsのソースコードを参照しながら読み進めるのであれば、GitHubページ上でtキーバインドを使用してfile finderを起動し、ファイルを素早く見つけることをお勧めします。
それではアプリケーションを起動して初期化を開始しましょう。Railsアプリケーションの起動はrails consoleまたはrails serverを実行して行うのが普通です。
railties/exe/railsrails serverのうち、railsコマンドの部分はRubyで記述された実行ファイルであり、読み込みパス上に置かれています。この実行ファイルには以下の行が含まれています。
version = ">= 0" load Gem.bin_path('railties', 'rails', version)
このコマンドをRailsコンソールで実行すると、railties/exe/railsが読み込まれるのがわかります。railties/exe/rails.rbファイルには以下のコードが含まれています。
require "rails/cli"
今度はrailties/lib/rails/cliファイルがRails::AppLoader.exec_appを呼び出します。
railties/lib/rails/app_loader.rbexec_appの主な目的は、Railsアプリケーションにあるbin/railsを実行することです。カレントディレクトリにbin/railsがない場合、bin/railsが見つかるまでディレクトリを上に向って探索します。これにより、Railsアプリケーション内のどのディレクトリからでもrailsコマンドを実行できるようになります。
rails serverについては、以下の同等のコマンドが実行されます。
$ exec ruby bin/rails server
bin/railsこのファイルの内容は次のとおりです。
#!/usr/bin/env ruby APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands'
APP_PATH定数は後でrails/commandsで使用されます。この行で参照されているconfig/bootファイルは、Railsアプリケーションのconfig/boot.rbファイルであり、Bundlerの読み込みと設定を担当します。
config/boot.rbconfig/boot.rbには以下の行が含まれています。
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile.
標準的なRailsアプリケーションにはGemfileというファイルがあり、アプリケーション内のすべての依存関係がそのファイル内で宣言されています。config/boot.rbはGemfileの位置をENV['BUNDLE_GEMFILE']に設定します。Gemfileが存在する場合、bundler/setupをrequireします。このrequireは、Gemfileの依存ファイルが置かれている読み込みパスをBundlerで設定する際に使用されます。
標準的なRailsアプリケーションは多くのgemに依存しますが、特に以下のgemに依存しています。
rails/commands.rbconfig/boot.rbの設定が完了すると、次にrequireするのはコマンドの別名を拡張するrails/commandsです。この状況ではARGV配列にserverだけが含まれており、以下のように受け渡しされます。
require_relative "command" aliases = { "g" => "generate", "d" => "destroy", "c" => "console" "s" => "server", "db" => "dbconsole" "r" => "runner", "t" => "test" } command = ARGV.shift command = aliases[command] || command Rails::Command.invoke command, ARGV
serverの代わりにsが渡されると、ここで定義されているaliasesの中からマッチするコマンドを探します。
rails/command.rb何らかのRailsコマンドを1つ入力すると、指定の名前空間内にコマンドがあるかどうかinvokeが探索を試み、見つかった場合はそのコマンドを実行します。
コマンドがRailsによって認識されない場合はRakeに引き継いで同じ名前で実行します。
以下のソースにあるように、Rails::Commandはargsが空の場合に自動的にヘルプを出力します。
module Rails::Command class << self def invoke(namespace, args = [], **config) namespace = namespace.to_s namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) namespace = "version" if %w( -v --version ).include? namespace if command = find_by_namespace(namespace) command.perform(namespace, args, config) else find_by_namespace("rake").perform(namespace, args, config) end end end end
serverコマンドが指定されると、Railsはさらに以下のコードを実行します。
module Rails module Command class ServerCommand < Base # :nodoc: def perform set_application_directory! Rails::Server.new.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 end end end
上のファイルは、config.ruファイルが見つからない場合に限り、Railsのルートディレクトリ (config/application.rbを指すAPP_PATHから2階層上のディレクトリ) に置かれます。このコードは続いてrails/commands/serverを実行します。これはRails::Serverクラスを設定するものです。
actionpack/lib/action_dispatch.rbAction DispatchはRailsフレームワークのルーティングを司るコンポーネントです。ルーティング、セッションおよび共通のミドルウェアなどの機能を提供します。
rails/commands/server_command.rbRails::Serverクラスはこのファイル内で定義されており、Rack::Serverを継承しています。Rails::Server.newを呼び出すと、rails/commands/server.rbのinitializeメソッドが呼び出されます。
def initialize(*) super set_environment end
最初にsuperが呼び出され、そこからRack::Serverのinitializeメソッドを呼び出します。
lib/rack/server.rbRack::Serverは、あらゆるRackベースのアプリケーション (Railsもその1つです) のための共通のサーバーインターフェイスを提供する役割を担います。
Rack::Serverのinitializeは、いくつかの変数を設定しているだけの簡単なメソッドです。
def initialize(options = nil) @options = options @app = options[:app] if options && options[:app] end
この場合optionsの値はnilになるので、このメソッドでは何も実行されません。
superがRack::Serverの中で完了すると、rails/commands/server_command.rbに制御が戻ります。この時点で、set_environmentがRails::Serverオブジェクトのコンテキスト内で呼び出されますが、一見したところ大した処理を行なっていないように見えます。
def set_environment ENV["RAILS_ENV"] ||= options[:environment] end
実際にはこのoptionsメソッドではきわめて多くの処理を実行しています。このメソッド定義はRack::Serverにあり、以下のようになっています。
def options @options ||= parse_options(ARGV) end
そしてparse_optionsは以下のように定義されています。
def parse_options(args) options = default_options # Don't evaluate CGI ISINDEX parameters. # http://www.meb.uni-bonn.de/docs/cgi/cl.html args.clear if ENV.include?("REQUEST_METHOD") options.merge! opt_parser.parse!(args) options[:config] = ::File.expand_path(options[:config]) ENV["RACK_ENV"] = options[:environment] options end
default_optionsでは以下を設定します。
def default_options super.merge( Port: ENV.fetch("PORT", 3000).to_i, Host: ENV.fetch("HOST", "localhost").dup, DoNotReverseLookup: true, environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup, daemonize: false, caching: nil, pid: Options::DEFAULT_PID_PATH, restart_cmd: restart_command) end
ENVにREQUEST_METHODキーがないので、その行はスキップできます。次の行ではopt_parserからのオプションをマージします。opt_parserはRack::Serverで明確に定義されています。
def opt_parser Options.new end
このクラスはRack::Serverで定義されていますが、異なる引数を扱うためにRails::Serverで上書きされます。Rails::Serverのparse!の冒頭部分は以下のようになっています。
def parse!(args) args, options = args.dup, {} option_parser(options).parse! args options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" options[:server] = args.shift options end
このメソッドはoptionsのキーを設定します。Railsはこれを使用して、どのようにサーバーを実行するかを決定します。initializeが完了すると、先ほど設定したAPP_PATHがrequireされたサーバーコマンドに制御が戻ります。
config/applicationrequire APP_PATHが実行されると、続いてconfig/application.rbが読み込まれます (APP_PATHがbin/railsで定義されていることを思い出しましょう)。この設定ファイルはRailsアプリケーションの中にあり、必要に応じて自由に変更できます。
Rails::Server#startconfig/applicationが読み込まれると、続いてserver.startが呼び出されます。このメソッド定義は以下のようになっています。
def start print_boot_information trap(:INT) { exit } create_tmp_directories setup_dev_caching log_to_stdout if options[:log_stdout] super ... end private def print_boot_information ... puts "=> Run `rails server -h` for more startup options" end def create_tmp_directories %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) end end def setup_dev_caching if options[:environment] == "development" Rails::DevCaching.enable_by_argument(options[:caching]) end end def log_to_stdout wrapped_app # アプリケーションにタッチしてロガーを設定 console = ActiveSupport::Logger.new(STDOUT) console.formatter = Rails.logger.formatter console.level = Rails.logger.level unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) end end
Rails初期化の最初の出力はここで行われます。このメソッドではINTシグナルのトラップが作成され、CTRL-Cキーを押すことでサーバープロセスが終了するようになります。コードに示されているように、ここではtmp/cache、tmp/pids、tmp/sessionsおよびtmp/socketsディレクトリが作成されます。rails serverに--dev-cachingオプションを指定して呼び出した場合は、development環境でのキャッシュをオンにします。最後にwrapped_appが呼び出されます。このメソッドは、ActiveSupport::Loggerのインスタンスの作成とアサインが行われる前に、Rackアプリケーションを作成する役割を担います。
superメソッドはRack::Server.startを呼び出します。このメソッド定義の冒頭は以下のようになっています。
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] # ラップされたアプリケーションにタッチすることで、config.ruが読み込まれてから # デーモン化されるようにする (chdirなど). 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
Railsアプリケーションとして興味深いのは、最終行にあるserver.runでしょう。ここでもwrapped_appメソッドが再び使われています。今度はこのメソッドをもう少し詳しく調べてみましょう (既に一度実行され、メモ化されてはいますが)。
@wrapped_app ||= build_app app
このappメソッドの定義は以下のようになっています。
def app @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end ... private 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) self.options.merge! options app end def build_app_from_string Rack::Builder.new_from_string(self.options[:builder]) end
options[:config]の値はデフォルトではconfig.ruです。config.ruには以下が含まれています。
# このファイルはRackベースのサーバーでアプリケーションの起動に使用される require_relative 'config/environment' run <%= app_const %>
上のコードのRack::Builder.parse_fileメソッドは、このconfig.ruファイルの内容を取り出し、以下のコードを使用して解析 (parse) します。
app = new_from_string cfgfile, config ... def self.new_from_string(builder_script, file="(rackup)") eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", TOPLEVEL_BINDING, file, 0 end
Rack::Builderのinitializeメソッドはこのブロックを受け取り、Rack::Builderのインスタンスの中で実行します。Railsの初期化プロセスの大半がこの場所で実行されます。config.ruのconfig/environment.rbのrequire行が最初に実行されます。
require_relative 'config/environment'
config/environment.rbこのファイルはconfig.ru (rails server)とPassengerの両方で必要となるファイルです。サーバーを実行するためのこれら2種類の方法はここで合流します。ここより前の部分はすべてRackとRailsの設定です。
このファイルの冒頭部分ではconfig/application.rbがrequireされます。
require_relative 'application'
config/application.rbこのファイルではconfig/boot.rbがrequireされます。
require_relative 'boot'
それまでにboot.rbがrequireされていなかった場合に限り、rails serverの場合にはboot.rbがrequireされます。ただしPassengerを使う場合にはboot.rbがrequireされません。
ここからいよいよ面白くなってきます。
config/application.rbの次の行は以下のようになっています。
require 'rails/all'
railties/lib/rails/all.rbこのファイルはRailsのすべてのフレームワークをrequireする役目を担当します。
require "rails" %w( active_record/railtie action_controller/railtie action_view/railtie action_mailer/railtie active_job/railtie action_cable/engine active_storage/engine rails/test_unit/railtie sprockets/railtie ).each do |railtie| begin require railtie rescue LoadError end end
ここでRailsのすべてのフレームワークが読み込まれ、アプリケーションから利用できるようになります。本章ではこれらのフレームワークの詳細については触れませんが、皆様にはぜひ自分でこれらのフレームワークを探索してみることをお勧めいたします。
現時点では、Railsエンジン、I18n、Rails設定などの共通機能がここで定義されていることを押さえておいてください。
config/environment.rbに戻るconfig/application.rbの残りの行ではRails::Applicationの設定を行います。この設定はアプリケーションの初期化が完全に完了してから使用されます。config/application.rbがRailsの読み込みを完了し、アプリケーションの名前空間が定義されると、制御はふたたびconfig/environment.rbに戻ります。ここではRails.application.initialize!によるアプリケーションの初期化が行われます。これはrails/application.rbで定義されています。
railties/lib/rails/application.rbそのinitialize!メソッドは以下のようなコードです。
def initialize!(group=:default) #:nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true self end
見てのとおり、アプリケーションの初期化は一度だけ行うことができます。railties/lib/rails/initializable.rbで定義されているrun_initializersメソッドによって各種イニシャライザが実行されます。
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はややトリッキーなコードになっています。Railsはここで、あらゆる先祖クラスをくまなく調べ、あるひとつのinitializersメソッドに応答するものを探しだしています。続いてそれらを名前でソートし、その順序で実行します。たとえば、Engineクラスはinitializersメソッドを提供しているので、あらゆるエンジンが利用できるようになります。
Rails::Applicationクラスはrailties/lib/rails/application.rbファイルで定義されており、その中でbootstrap、railtie、finisherイニシャライザをそれぞれ定義しています。bootstrapイニシャライザは、ロガーの初期化などアプリケーションの準備を行います。一方、最後に実行されるfinisherイニシャライザはミドルウェアスタックのビルドなどを行います。railtieイニシャライザはRails::Application自身で定義されており、bootstrapとfinishersの間に実行されます。
これが完了したら、制御はRack::Serverに移ります。
appメソッドが定義されている箇所は、最後に見た時は以下のようになっていました。
def app @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end ... private 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) self.options.merge! options app end def build_app_from_string Rack::Builder.new_from_string(self.options[:builder]) end
このコードにおけるappとは、Railsアプリケーション自身 (ミドルウェア) であり、
その後では、提供されているすべてのミドルウェアをRackが呼び出します。
def build_app(app) middleware[options[:environment]].reverse_each do |middleware| middleware = middleware.call(self) if middleware.respond_to?(:call) next unless middleware klass = middleware.shift app = klass.new(app, *middleware) end app end
ここで、Server#startの最終行でbuild_appが (wrapped_appによって) 呼び出されていたことを思い出しましょう。最後に見かけたときのコードは以下のようになっていました。
server.run wrapped_app, options, &blk
ここで使われているserver.runの実装は、アプリケーションで使うWebサーバーに依存します。たとえばPumaを使用している場合、runメソッドは以下のようになります。
... DEFAULT_OPTIONS = { :Host => '0.0.0.0', :Port => 8080, :Threads => '0:16', :Verbose => false } def self.run(app, options = {}) options = DEFAULT_OPTIONS.merge(options) if options[:Verbose] app = Rack::CommonLogger.new(app, STDOUT) end if options[:environment] ENV['RACK_ENV'] = options[:environment].to_s end server = ::Puma::Server.new(app) min, max = options[:Threads].split(':', 2) puts "Puma #{::Puma::Const::PUMA_VERSION} starting..." puts "* Min threads: #{min}, max threads: #{max}" puts "* Environment: #{ENV['RACK_ENV']}" puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}" server.add_tcp_listener options[:Host], options[:Port] server.min_threads = min server.max_threads = max yield server if block_given? begin server.run.join rescue Interrupt puts "* Gracefully stopping, waiting for requests to finish" server.stop(true) puts "* Goodbye!" end end
本章ではサーバーの設定自体については深入りしませんが、この箇所はRailsの初期化プロセスという長い旅の最後のひとかけらです。
本章で解説した高度な概要は、自分が開発したコードがいつどのように実行されるかを理解するためにも、そしてより優れたRails開発者になるためにも役に立つことでしょう。もっと詳しく知りたいのであれば、次のステップとしてRailsのソースコードそのものを追うのがおそらく最適です。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。