猫の魔法

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

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

はじめに

railsの起動までを見ていこうその4ということで、今回は rails new で生成される config.ru から見ていこうと思います。 以下の「railsの起動プロセスを追う」の部分に各回へのリンクがあります。

rails調査系まとめ - 猫の魔法

※この起動プロセスについてはrails公式のドキュメントがあります。詳しくはこちらをご覧下さい。

run

前回までで、下記の require_relative 'config/environment' の解析を終えたので、今回は run Rails.application から見ていきます。

# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

run Rails.application

Rails.applicationの部分はその3で調査した下記コードから Rails::Application を継承したクラスのインスタンスが入ります。

rails/rails.rb at v5.2.1 · rails/rails · GitHub

    def application
      @application ||= (app_class.instance if app_class)
    end

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
  • ./config/application.rb
module TestRails
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

run 自体は引数に渡ってきた Rails::application@app にセットするだけです。

rack/builder.rb at 2.0.5 · rack/rack · GitHub

    # Takes an argument that is an object that responds to #call and returns a Rack response.
    # The simplest form of this is a lambda object:
    #
    #   run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
    #
    # However this could also be a class:
    #
    #   class Heartbeat
    #     def self.call(env)
    #      [200, { "Content-Type" => "text/plain" }, ["OK"]]
    #     end
    #   end
    #
    #   run Heartbeat
    def run(app)
      @run = app
    end

new_from_string の評価結果

これでやっと、 new_from_string 内の Rack::Builder.new {\n" + builder_script + "\n}} が何を返してくるのかが分かりました。 ※builder_scriptはconfig.ruの内容と等価です。

次の関心事は、Rack::Builder.new {\n" + builder_script + "\n}.to_app が何をしているかです。 to_app の実体は railties/lib/rails/application.rb にあります。 この処理では自分自身、つまりRails::Application を継承したクラスのインスタンスが返されます。

rails/application.rb at v5.2.1 · rails/rails · GitHub

    def to_app #:nodoc:
      self
    end

よって、new_from_stringRails::Application を継承したクラスのインスタンスが返ることになります。 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

ということで wrapped_appapp が返す値はself.parse_file -> build_app_and_options_from_config -> app->app から、Rails::Application を継承したクラスのインスタンスが返ります。

再びwrapped_app

ここまでで、 wrapped_app 内の app が何を返すかが分かりました。 次に、 build_app が何をしているのかを見ていきます。

rack/server.rb at 2.0.5 · rack/rack · GitHub

      def wrapped_app
        @wrapped_app ||= build_app app
      end

build_appの内容は以下のようになっています。

rack/server.rb at 2.0.5 · rack/rack · GitHub

      def build_app(app)
        middleware[options[:environment]].reverse_each do |middleware|
          middleware = middleware.call(self) if middleware.respond_to?(:call)
          next unless middleware
          klass, *args = middleware
          app = klass.new(app, *args)
        end
        app
      end

何やらいろいろと処理をしていますが、 middleware は継承先のクラスで以下のように定義されていて、このメソッド自体は 引数のappの内容をそのまま返します。

rails/server_command.rb at v5.2.1 · rails/rails · GitHub

    def middleware
      Hash.new([])
    end

これで再びstartメソッドの処理を追うことに戻れます。

再びstart

以下に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 が行っていることは判明しています。 次の daemonize_app は デーモン化をしているだけなので、特に読むところはありません。 その次の write_pid はサーバが二重起動しないようにする為のpidが入ったロックファイルを作成するという物で、コード的には読むところはあるのですが、趣旨と外れるので飛ばします。 trap(:INT)のブロックもその3で説明した Ctrl-Cのときの動作を定義する物なので飛ばします。

最後の server.run wrapped_app, options, &blkは、今まで待ちに待ったサーバを起動する部分になります。

サーバの起動。

serverについては、rack側には以下の定義があります。 一般的には、@_server に起動するサーバの実体が入ってくるようにします。

rack/server.rb at 2.0.5 · rack/rack · GitHub

    def server
      @_server ||= Rack::Handler.get(options[:server])

      unless @_server
        @_server = Rack::Handler.default

        # We already speak FastCGI
        @ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
      end

      @_server
    end

例えば、Pumaの場合 Rack::Handler.defaultRack::Handler::Puma が入ってきます。 Puma側では下記のように self.run メソッドが定義されていて、Pumaが起動されるようになっています。

puma/puma.rb at master · puma/puma · GitHub

      def self.run(app, options = {})
        conf   = self.config(app, options)

        events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio

        launcher = ::Puma::Launcher.new(conf, :events => events)

        yield launcher if block_given?
        begin
          launcher.run
        rescue Interrupt
          puts "* Gracefully stopping, waiting for requests to finish"
          launcher.stop
          puts "* Goodbye!"
        end
      end

これでやっと、一連のrailsの起動プロセスを追うことが出来ました。

まとめ

  • 起動の際に使うアプリケーションは Rails::Application を継承したクラスのインスタンスを使っている。
  • 起動するサーバはアプリケーションサーバ側で runメソッドを定義してある物を実行する。