猫の魔法

主にruby系の技術メモを記載

railsの起動プロセスを追う その3

はじめに

また間が空いてしまいましたが、 railsの起動までを見ていこう第3段ということで、今回は rails/all.rb から見ていこうと思います。 以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。

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 inheritedbase には下記の 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_configconfig.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_scriptRack::Builder.new のブロックに展開した上で、to_app した内容を最終的にメソッドの戻り値としています。 builder_scriptconfig.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.ruDSLを解析する為に、 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 クラスマクロはクラスインスタンス変数である initializersInitializer クラスのインスタンスを追加しています。 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

この処理を読む上で重要なのは iInitializer クラスのインスタンスであるということです。 Initializer クラスはインスタンスメソッドとして、bindを持っているので、下記の処理が呼ばれることになります。 @context は特異メソッドである initializerInitializer クラスのインスタンスを作成した際は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_eachrubyのトポジカルソートのライブラリです(今回はちゃんと調べる気がないので、一旦飛ばします。ごめんなさい) 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 クラスマクロで定義された初期化を行っている。