アプリケーションの処理フローを追ってみる

今回調査すること

  • Requestがどのように受け渡されるか
  • ActionControllerのバージョン: 2.3.5

ご参考

Railsノート - ActionController::Request の生成過程を Webサーバーまでさかのぼる

こっちのが詳しく書かれています。いろいろ勉強になります。

最初の流れ

httpリクエストの簡単な流れは,リクエスト=>処理=>レスポンスだ。
その受け渡しの部分を順番に見ていく。

リクエスト

まずは、HTTPリクエストを受け取り、処理側に渡す部分。
Railsでは今Rackが標準で利用されている。詳しく調べてないけどRackはHTTPサーバ(MongrelやPassenger)とアプリケーションの間で動くミドルウェアでアプリケーションから見たリクエストをどのHTTPサーバから来ても同一のものに変更してくれるものだと認識している。

Rackのサンプル

こうするとMongrelを起動してポート3000を利用して起動する。

require "rubygems"
require "rack"

class HelloRack
  def call(env)
    [200, {"Content-Type" => "text/plain"}, ["Hello"]]
  end
end

Rack::Handler::Mongrel.run HelloRack.new, :Port => 3000

起動すると以下のテストをパスする。

require "rubygems"
require "expectations"
require "net/http"


Expectations do
  expect "Hello" do
    Net::HTTP.get("localhost", "/", 3000)
  end
end


ということは実質、Rack::Handler::Mongrel.runの処理を見ればいいことになる。

Rack::Handler::Mongrel.run

コード見る通りoption解析してリクエストを受け付ける。

module Rack                                                                                                                                                                                                 
  module Handler                                                                                                                                                                                            
    class Mongrel < ::Mongrel::HttpHandler                                                                                                                                                                  
      def self.run(app, options={})                                                                                                                                                                         
        server = ::Mongrel::HttpServer.new(options[:Host] || '0.0.0.0',                                                                                                                                     
                                           options[:Port] || 8080)  

                                     .
                                     .
                                     .
                                     .
 
        server.register('/', Rack::Handler::Mongrel.new(app))                                                                                                                                             
        server.run.join                                                                                                                                                                                     
      end       
  end
end

Mongrelについて昔ちょっと調べたので調べない。
要は、以下のことがいえればまずはいいとおもう。

Rackを起動すると、任意のHTTPサーバがリクエスト受付を開始する。
リクエストが来た際はそれをアプリケーションに渡し、スレッドに処理を任せる。
処理が終わった後、レスポンスを返す。

少しリクエスト=>処理=>レスポンスの流れが明確になったかな。
で、今回はRailsの処理だから、RackがRailsにどのようにリクエストを渡しているのか見てみる。

Railsアプリの起動

Rails開発者であればきっと誰もがしたことであろう、アプリ起動の流れからリクエストの受付を見ていきます。

ruby script/server

を追っていきます。

script/server

中身は本当に簡単。

#!/usr/bin/env ruby                                                                                                                                                                                          
require File.expand_path('../../config/boot',  __FILE__)                                                                                                                                                     
require 'commands/server' 

config/bootの所はよんだことあるけどまだブログにのせてないので後でのせる。
多分サーバ起動してるところはserver.rbであると推測できる。

server.rb

Railsに内包されている。(lib/commands/server.rb)
コードが意外と長いので、流れだけ書く。

1. オプション解析
2. Rackから利用サーバを取得
3. RAILS_ROOT/tmp/ 以下にアプリ利用ディレクトリを作成する
4. RAILS_ROOT/config/environment.rbを読み込みActionController::Dispatcheを生成(RAILS_ROOT以下にconfig.ruがあればそれを優先する)
5. Prefixの設定
6. Rack::Builderを利用してprefixに対してDispathcerを登録する
7. Server起動

リクエストの受付を処理している部分は上のフローの456なのでそこだけ順番に見ていく。

Dispatcher.new

Rack application objectを返す。あまり深入りしない。

Rack::Builder.new

ここはちょっとコードを見てみる。

 app = Rack::Builder.new {                                                                                                                                                                                  
   use Rails::Rack::LogTailer unless options[:detach]                                                                                                                                                       
   use Rails::Rack::Debugger if options[:debugger]                                                                                                                                                          
   map map_path do                                                                                                                                                                                          
     use Rails::Rack::Static                                                                                                                                                                                
    run inner_app                                                                                                                                                                                          
  end                                                                                                                                                                                                      
}.to_app  

Rack::Builder.newの後のブロックはinstance_evalが呼ばれているだけである。
map map_path 内で行われているのはpathに対するアプリケーションの動作である。
ここでまたRails::Rack::Staticってやつがいろいろしてそうなので見に行く。

先に結論ぽい感じのことを理解できた気がするのでかいておく。

ここでは、Rackに対してRailsアプリケーションの動作を登録する。
アプリケーション起動時のパスに対して、アプリケーションの動作を登録する。

RackとRailsアプリケーションのパスの認識はRails::Rack::Staticにより行われる。
認識後、Dispatherに処理を渡してアプリケーションを呼ぶ。
Rails::Rack::Static

一部抜粋すると以下のような実装になっている。

module Rails
  module Rack
    class Static
      FILE_METHODS = %w(GET HEAD).freeze

      def initialize(app)
        @app = app
        @file_server = ::Rack::File.new(File.join(RAILS_ROOT, "public"))
      end

      def call(env)
        path        = env['PATH_INFO'].chomp('/')
        method      = env['REQUEST_METHOD']

        if FILE_METHODS.include?(method)
          if file_exist?(path)
            return @file_server.call(env)
          else
            cached_path = directory_exist?(path) ? "#{path}/index" : path
            cached_path += ::ActionController::Base.page_cache_extension

            if file_exist?(cached_path)
              env['PATH_INFO'] = cached_path
              return @file_server.call(env)
            end
          end
        end

        @app.call(env)
      end
    end
  end
end

ざっくりいうと静的ファイルか動的ファイルかを判断してそれに応じてレシーバを変えている。
public以下はファイルサーバとして、それ以外はDispatherに処理を依存する。

じゃあ後はさっきおいておいたDispatcherの動きを見れば良い。

ActionController::Dispather

なんか昔はRAILS_ROOT/publicにおいていた気もするんだけどいつの間にかなくなってたな…
initializeから動きを追ってみる。

def initialize(output = $stdout, request = nil, response = nil)                                                                                                                                        
      @output = output                                                                                                                                                                                     
      build_middleware_stack if @@cache_classes                                                                                                                                                            
 end

どうってことない。次。

      def build_middleware_stack                                                                                                                                                                           
        @app = @@middleware.build(lambda { |env| self.dup._call(env) })                                                                                                                                    
      end          

この@@middlewareがRack ObjectにRailsの処理内容をつめたものである。(多分)
このmiddlewareはクラス変数として次のように定義される。

    self.middleware = MiddlewareStack.new do |middleware|                                                                                                                                                  
      middlewares = File.join(File.dirname(__FILE__), "middlewares.rb")                                                                                                                                    
      middleware.instance_eval(File.read(middlewares))                                                                                                                                                     
    end 

middleware_stackはただのArrayを継承したクラスなんだけど、buildするとmiddlewareクラスを生成する。
そこにRailsとして必要な処理の内容をinstance_eval利用して定義する。(File.readのとこ)
きっとイメージは以下のようになっているはず。

  middleware_stack = [middleware(Rack::Lock), middleware(ActionController::Failfalse), middleware(ActionController::ParamsParser)]

実行時はこういったフィルター的なものをかまして処理していくんじゃないかなと思う。
build_middleware_stackにもどる。
lambdaの中では、実際の処理を登録するとこだろう。Routing::Routes.call(@env)でアプリケーション側の呼び先を指定して、callで呼ぶ

まとめ

全体の流れはきっと次のようなものかと。

                                              • -
Webrick | Mongrel | FastCGI
                                              • -
Rack
                                              • -
Dispather
                                              • -
Controller
                                              • -

rubyのProc#arity

初めて知った。Procを渡される際の引数の個数を管理するようだ。

require "rubygems"
require "expectations"

def block_test(&block)
  return block.arity
end

Expectations do
  expect 1 do
    block_test { |hoge| puts "hoge" }
  end

  expect 2 do
    block_test { |hoge, moge| puts "hoge" }
  end
end