rack を理解する

https://github.com/rack/rack

rack は Ruby web application 開発のためにいい感じのインターフェースを提供してくれる。
HTTP Request/Response をラップしてくれることで、web server や web framework、その間にある middleware の API を 1 つのメソッド呼び出しにまとめてくれる。
port を開いたり、connection を受け付けたりといったこともしてくれて、web server とのやり取りを引き受けてくれるので開発者はアプリケーション開発に集中できる。

rack アプリケーションの基本

rack application は call を呼び出せる Ruby オブジェクト。環境を引数に取って、status、header、body の配列を返す。
call の引数である envCGI-like な header を持った unfrozen な Hash オブジェクトで rack アプリケーションはこの env を変えてもよい。

sample

rackup に引数として Rack::Builder DSL を書いた config.ru を渡す。
POST メソッドのみ受け付けて、リクエストボディをそのまま返す rack アプリケーションとその rack アプリケーションを 3 つの middleware で覆っている。
それぞれの middleware では呼ばれた順番とその時のアプリケーションを標準エラー出力している。

class Empty
  def initialize(app)
    @app = app
  end

  def call(env)
    p "Before app: Empty"
    p @app

    res = @app.call(env)

    p "After app: Empty"

    res
  end
end

class PostDake
  class Error < StandardError; end

  def initialize(app)
    @app = app
  end

  def call(env)
    p "Before app: PostDake"
    p @app

    raise Error unless env["REQUEST_METHOD"] == "POST"

    res = @app.call(env)

    p "After app: PostDake"

    res
  rescue Error => e
    [ 500,
      {"Content-Type" => "text/plain"},
      ["#{env['REQUEST_METHOD']} is not accepted!"],
    ]
  end
end

class Big
  def initialize(app)
    @app = app
  end

  def call(env)
    p "Before app: Big"
    p @app

    res = @app.call(env)

    p "After app: Big"

    [ 200,
      {"Content-Type" => "text/plain"},
      res[2].map {|s| s.upcase}
    ]
  end
end

class ReturnBody
  require "rack"

  def call(env)
    req = Rack::Request.new(env)
    [ 200,
      {"Content-Type" => "text/plain"},
      [req.body.read]
    ]
  end
end

use Empty
use PostDake
use Big
run ReturnBody.new

この config.ru を置いているディレクトリで rack アプリケーションを起動して curl すると下記のように標準エラー出力する。 ここから以下のようなことが分かる。

  • 処理の流れ
    • -(request)-> Empty -> PostDake -> Big -> ReturnBody(アプリケーション本体) -> Big -> PostDake -> Empty -(response)->
    • request では上から use した順番で middleware の処理を行ってアプリケーション本体(ReturnBody オブジェクト)に request が到達する。
    • response では反対に下から use した順番で middleware の処理を行って response を返す。
  • app の状態
    • app には処理が行われる rack middleware が rack アプリケーションに至るまで入っている
    • curl した時のそれぞれの標準エラー出力
      • Empty の中で出力した app #<PostDake:0x00007feeb1801c70 @app=#<Big:0x00007feeb1801cc0 @app=#<ReturnBody:0x00007feeb1801e00>>>
        • Empty 次には PostDake に処理が渡る。PostDake の次は Big
      • PostDake の中で出力した app #<Big:0x00007feeb1801cc0 @app=#<ReturnBody:0x00007feeb1801e00>>
      • Big の中で出力した app #<ReturnBody:0x00007feeb1801e00>
        • Big の次は rack アプリケーションに処理が到達するので、次のオブジェクトは @app を持っていない
❯ rackup
Puma starting in single mode...
* Version 4.3.3 (ruby 2.6.5-p114), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop
"Before app: Empty"
#<PostDake:0x00007feeb1801c70 @app=#<Big:0x00007feeb1801cc0 @app=#<ReturnBody:0x00007feeb1801e00>>>
"Before app: PostDake"
#<Big:0x00007feeb1801cc0 @app=#<ReturnBody:0x00007feeb1801e00>>
"Before app: Big"
#<ReturnBody:0x00007feeb1801e00>
"After app: Big"
"After app: PostDake"
"After app: Empty"
::1 - - [04/Apr/2020:10:54:48 +0900] "POST / HTTP/1.1" 200 10 0.0051

rails も rack 使って web サーバーを起動している

コードを追う

example のコマンド bin/rackup -Ilib example/lobster.ru で rack application を立ち上げるところを追う

Rack::Server

  • bin/rackup は Rack::Server.start を実行するだけ
  • Rack::Server.start
    • コンストラクタに引数(options)をそのまま渡してオブジェクトにstartさせている
  • Rack::Server#new
    • 起動オプションがあれば使ってなければ、なければ default_option を使う
    • Rack::Server#parse_options を実行して options[:config] に example/config.ru絶対パスを代入する
  • Rack::Server#start
    • オプションを設定して server.run を実行
      • Rack::Server.get または Rack::Server.default を使って、@_server に Handler を代入する

Rack::Server#wrapped_app

  • Rack::Server#wrapped_app
    • Rack::Server#build_up(Rack::Server#app) を実行している
      • Rack::Server#build_up
        • 環境ごとの middleware を Array#reverse_each していく
          • middlewareは、環境ごとにデフォルトでいくつか入っている
          • Rack::Server#app で生成された rack アプリケーションを 環境ごとのデフォルトの middleware でラップしていく
      • Rack::Server#app
        • ここでは options[:config] = confing.ru の前提で進めると、Rack::Builder.parse_file に config.ru を渡す

Rack::Builder

  • Rack::Builder#parse_file
    • File.read(config.ru) を実行した文字列を Rack::Builder.new_from_string に渡す
  • Rack::Builder.new_from_string
    • Rack::Builder オブジェクトのコンテキストで、つまり Rack::Builder DSL で config.ru を評価する(instance_eval)。評価して Rack::Builder#to_app を呼ぶ
      • Rack::Builder#use Rack::Builder#run が主なメソッド
        • Rack::Builder#use
          • config.ru 中に use <Rack middleware クラス> という DSL があるときに呼ばれる
          • @use の配列に Proc オブジェクトを append する
            • Proc オブジェクト: app を引数に取って、その app を引数として rack middleware をインスタンス化する
        • Rack::Builder#run
          • config.ru 中に run <Rack middleware オブジェクト> という DSL があるときに呼ばれる
          • @run に app を代入する
    • Rack::Builder#to_app
      • app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } }
        • use した middleware と run した rack アプリケーションを入れ子構造にしていく
          • @use は app を引数に取って middleware をインスタンス化する proc オブジェクトの配列
          • >>

        • 最終的に入れ子構造になった rack アプリケーションを返す。

まとめ

  • rack は Rack::Builder に定義された DSL を使って web サーバーを立ち上げてくれる
    • サーバーは cgi, webrick など Rack::Handler で定義されたものを使える
    • use で宣言した順番で middleware の http request を受け付ける
    • run で宣言した rack アプリケーションが http request を受け取ってそこから middleware を逆順にたどって response が返される