Rails の初期化プロセス

本章は、Rails 4におけるRails初期化プロセスの内部について解説します。上級Rails開発者向けに推奨される、きわめて高度な内容を扱っています。

このガイドの内容:

本章では、デフォルトのRails 4アプリケーション向けにRuby on Railsスタックの起動時に必要となるすべてのメソッド呼び出しについて、詳細な解説を行います。具体的には、rails serverを実行してアプリケーションを起動したときにどのようなことが行われているかに注目して解説します。

文中に記載されるRuby on Railsアプリケーションへのパスは、特に記載のない限り相対パスを使用します。

Railsのソースコードを参照しながら読み進めるのであれば、Githubページ上でtキーバインドを使用してfile finderを起動し、ファイルを素早く見つけることをお勧めします。

1 起動!

それではアプリケーションを起動して初期化を開始しましょう。Railsアプリケーションの起動はrails consoleまたはrails serverを実行して行うのが普通です。

1.1 railties/bin/rails

rails serverのうち、railsコマンドの部分はRubyで記述された実行ファイルであり、読み込みパス上に置かれています。この実行ファイルには以下の行が含まれています。

version = ">= 0"
load Gem.bin_path('railties', 'rails', version)

このコマンドをRailsコンソールで実行すると、railties/bin/railsが読み込まれるのがわかります。railties/bin/rails.rbファイルには以下のコードが含まれています。

require "rails/cli"

今度はrailties/lib/rails/cliファイルがRails::AppRailsLoader.exec_app_railsを呼び出します。

1.2 railties/lib/rails/app_rails_loader.rb

exec_app_railsの主要な目的な、Railsアプリケーションにあるbin/railsを実行することです。カレントディレクトリにbin/railsがない場合、bin/railsが見つかるまでディレクトリを上に向って探索します。これにより、Railsアプリケーション内のどのディレクトリからでもrailsコマンドを実行できるようになります。

rails serverについては、以下の同等のコマンドが実行されます。

$ exec ruby bin/rails server

1.3 bin/rails

このファイルの内容は次のとおりです。

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'

APP_PATH定数は後でrails/commandsで使用されます。この行で参照されているconfig/bootファイルは、Railsアプリケーションのconfig/boot.rbファイルであり、Bundlerの読み込みと設定を担当します。

1.4 config/boot.rb

config/boot.rbには以下の行が含まれています。

# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

標準的なRailsアプリケーションにはGemfileというファイルがあり、アプリケーション内のすべての依存関係がそのファイル内で宣言されています。config/boot.rbはGemfileの位置をENV['BUNDLE_GEMFILE']に設定します。Gemfileが存在する場合、bundler/setupをrequireします。このrequireは、Gemfileの依存ファイルが置かれている読み込みパスをBundlerで設定する際に使用されます。

標準的なRailsアプリケーションは多くのgemに依存しますが、特に以下のgemに依存しています。

  • actionmailer
  • actionpack
  • actionview
  • activemodel
  • activerecord
  • activesupport
  • arel
  • builder
  • bundler
  • erubis
  • i18n
  • mail
  • mime-types
  • rack
  • rack-cache
  • rack-mount
  • rack-test
  • rails
  • railties rake
  • sqlite3
  • thor
  • tzinfo

1.5 rails/commands.rb

config/boot.rbの設定が完了すると、次にrequireするのはコマンドの別名を拡張するrails/commandsです。この状況でARGV配列にはserverだけが含まれており、以下のように受け渡しされます。

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)

実際にやってみるとわかるとおり、空のARGVリストが渡されると、使用法のスニペットが表示されます。

serverの代わりにsが渡されると、ここで定義されているaliasesの中からマッチするコマンドを探します。

1.6 rails/commands/command_tasks.rb

run_commandは、間違ったRailsコマンドが入力された時にエラーメッセージを表示する役割も担います。正しいコマンドの場合は同じ名前のメソッドが呼び出されます。

COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help)

def run_command!(command)
  command = parse_command(command)
  if COMMAND_WHITELIST.include?(command)
    send(command)
  else
    write_error_message(command)
  end
end

serverコマンドが指定されると、Railsはさらに以下のコードを実行します。

def set_application_directory!
  Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
end

def server
  set_application_directory!
  require_command!("server")

  Rails::Server.new.tap do |server|
    # サーバーが環境を設定してからアプリケーションをrequireする必要がある
    # そうしないとサーバーに与えられた環境オプションを展開できない
    require APP_PATH
    Dir.chdir(Rails.application.root)
    server.start
  end
end

def require_command!(command)
  require "rails/commands/#{command}"
end

上のファイルは、config.ruファイルが見つからない場合に限り、Railsのルートディレクトリ (config/application.rbを指すAPP_PATHから2階層上のディレクトリ) に置かれます。このコードは続いてrails/commands/serverを実行します。これはRails::Serverクラスを設定するものです。

require 'fileutils'
require 'optparse'
require 'action_dispatch'
require 'rails'

module Rails
  class Server < ::Rack::Server

fileutilsおよびoptparseは標準のRubyライブラリであり、それぞれファイル操作や解析オプションを使用できるヘルパー関数を提供します。

1.7 actionpack/lib/action_dispatch.rb

Action DispatchはRailsフレームワークのルーティングを司るコンポーネントです。ルーティング、セッションおよび共通のミドルウェアなどの機能を提供します。

1.8 rails/commands/server.rb

Rails::Serverクラスはこのファイル内で定義されており、Rack::Serverを継承しています。Rails::Server.newを呼び出すと、rails/commands/server.rbinitializeメソッドが呼び出されます。

def initialize(*)
  super
  set_environment
end

最初にsuperが呼び出され、そこからRack::Serverinitializeメソッドを呼び出します。

1.9 Rack: lib/rack/server.rb

Rack::Serverは、あらゆるRackベースのアプリケーション (Railsもその中に含まれます) のための共通のサーバーインターフェイスを提供する役割を担います。

Rack::Serverinitializeは、いくつかの変数を設定しているだけの簡単なメソッドです。

def initialize(options = nil)
  @options = options
  @app = options[:app] if options && options[:app]
end

この場合optionsの値はnilになるので、このメソッドでは何も実行されません。

superRack::Serverの中で完了すると、rails/commands/server.rbに制御が戻ります。この時点で、set_environmentRails::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

  # CGI ISINDEXパラメータをevaluateしないこと
  # 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
  environment  = ENV['RACK_ENV'] || 'development'
  default_host = environment == 'development' ? 'localhost' : '0.0.0.0'

  {
    :environment => environment,
    :pid         => nil,
    :Port        => 9292,
    :Host        => default_host,
    :AccessLog   => [],
    :config      => "config.ru"
  }
end

ENVREQUEST_METHODキーがないので、その行はスキップできます。次の行ではopt_parserからのオプションをマージします。opt_parserRack::Serverで明確に定義されています。

def opt_parser
  Options.new
end

このクラスはRack::Serverで定義されていますが、異なる引数を扱うためにRails::Serverで上書きされます。Rails::Serverparse!の冒頭部分は以下のようになっています。

def parse!(args)
  args, options = args.dup, {}

  opt_parser = OptionParser.new do |opts|
    opts.banner = "Usage: rails server [mongrel, thin, etc] [options]"
    opts.on("-p", "--port=port", Integer,
            "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
  ...

このメソッドはoptionsのキーを設定します。Railsはこれを使用して、どのようにサーバーを実行するかを決定します。initializeが完了すると、rails/serverに戻ります。ここでは先ほど設定されたAPP_PATHがrequireされます。

1.10 config/application

require APP_PATHが実行されると、続いてconfig/application.rbが読み込まれます (APP_PATHbin/railsで定義されていることを思い出しましょう)。この設定ファイルはRailsアプリケーションの中にあり、必要に応じて自由に変更することができます。

1.11 Rails::Server#start

config/applicationが読み込まれると、続いてserver.startが呼び出されます。このメソッド定義は以下のようになっています。

def start
  print_boot_information
  trap(:INT) { exit }
  create_tmp_directories
  log_to_stdout if options[:log_stdout]

  super
  ...
end

private

  def print_boot_information
    ...
    puts "=> Run `rails server -h` for more startup options"
    ...
    puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
  end

  def create_tmp_directories
    %w(cache pids sessions sockets).each do |dir_to_make|
      FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
    end
  end

  def log_to_stdout
    wrapped_app # アプリにタッチしてロガーを設定

    console = ActiveSupport::Logger.new($stdout)
    console.formatter = Rails.logger.formatter
    console.level = Rails.logger.level

    Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
  end

Rails初期化の最初の出力が行われるのがこの箇所です。このメソッドではINTシグナルのトラップが作成され、CTRL-Cキーを押すことでサーバープロセスが終了するようになります。コードに示されているように、ここではtmp/cachetmp/pidstmp/sessionsおよびtmp/socketsディレクトリが作成されます。続いて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 ::File.expand_path('../config/environment', __FILE__)
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::Builderinitializeメソッドはこのブロックを受け取り、Rack::Builderのインスタンスの中で実行します。Railsの初期化プロセスの大半がこの場所で実行されます。config.ruconfig/environment.rbrequire行が最初に実行されます。

require ::File.expand_path('../config/environment', __FILE__)

1.12 config/environment.rb

このファイルはconfig.ru (rails server)とPassengerの両方で必要となるファイルです。サーバーを実行するためのこれら2種類の方法はここで出会います。ここより前の部分はすべてRackとRailsの設定です。

このファイルの冒頭部分ではconfig/application.rbがrequireされます。

require File.expand_path('../application', __FILE__)

1.13 config/application.rb

このファイルではconfig/boot.rbがrequireされます。

require File.expand_path('../boot', __FILE__)

それまでにboot.rbがrequireされていなかった場合に限り、rails serverの場合にはboot.rbがrequireされます。ただしPassengerを使用する場合にはboot.rbがrequire*されません*。

ここからいよいよ面白くなってきます。

2 Railsを読み込む

config/application.rbの次の行は以下のようになっています。

require 'rails/all'

2.1 railties/lib/rails/all.rb

このファイルはRailsのすべてのフレームワークをrequireする役目を担当します。

require "rails"

%w(
  active_record
  action_controller
  action_view
  action_mailer
  rails/test_unit
  sprockets
).each do |framework|
  begin
    require "#{framework}/railtie"
  rescue LoadError
  end
end

ここでRailsのすべてのフレームワークが読み込まれ、アプリケーションから利用できるようになります。本章ではこれらのフレームワークの詳細については触れませんが、皆様にはぜひ自分でこれらのフレームワークを探索してみることをお勧めいたします。

現時点では、Railsエンジン、I18n、Rails設定などの共通機能がここで定義されていることを押さえておいてください。

2.2 config/environment.rbに戻る

config/application.rbの残りの行ではRails::Applicationの設定を行います。この設定はアプリケーションの初期化が完全に完了してから使用されます。config/application.rbがRailsの読み込みを完了し、アプリケーションの名前空間が定義されると、制御はふたたびconfig/environment.rbに戻ります。ここではアプリケーションの初期化が行われます。たとえばアプリケーションの名前がBlogであれば、environment.rbにRails.application.initialize!という行があります。これはrails/application.rbで定義されています。

2.3 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ファイルで定義されており、その中でbootstraprailtiefinisherイニシャライザをそれぞれ定義しています。bootstrapイニシャライザは、ロガーの初期化などアプリケーションの準備を行います。一方、最後に実行されるfinisherイニシャライザはミドルウェアスタックのビルドなどを行います。railtieイニシャライザはRails::Application自身で定義されており、bootstrapfinishersの間に実行されます。

これが完了したら、制御はRack::Serverに移ります。

2.4 Rack: lib/rack/server.rb

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の実装は、アプリケーションで使用しているサーバーに依存します。たとえば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のソースコード自身を追うのがおそらく最適でしょう。