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 の引数である env
は CGI-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 を持っていない
- Empty の中で出力した 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 サーバーを起動している
- Rails::Command::ServerCommand#perform で Rails サーバーの起動をしていて、その中で Rails::Server#start を呼んでいる
- https://github.com/rails/rails/blob/30349de203de442676e162d3f3dd0492b29d19ac/railties/lib/rails/commands/server/server_command.rb#L147
- 環境変数を取ってきたり起動前のセットアップをしている
- Rails::Server#initialize を呼んだ後、そのインスタンスに対して Rails::Server#start を呼んでいる
- Rails::Server#initialize
- https://github.com/rails/rails/blob/30349de203de442676e162d3f3dd0492b29d19ac/railties/lib/rails/commands/server/server_command.rb#L12
- super() で Rack::Server#initialize に委譲している
- Rails::Server#start
- https://github.com/rails/rails/blob/30349de203de442676e162d3f3dd0492b29d19ac/railties/lib/rails/commands/server/server_command.rb#L21
- super() を呼んで、Rack::Server#start を呼び出している
- Rails::Server#initialize
コードを追う
example のコマンド bin/rackup -Ilib example/lobster.ru
で rack application を立ち上げるところを追う
Rack::Server
bin/rackup
は Rack::Server.start を実行するだけ- Rack::Server.start
- コンストラクタに引数(options)をそのまま渡してオブジェクトに
start
させている
- コンストラクタに引数(options)をそのまま渡してオブジェクトに
- 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 でラップしていく
- 環境ごとの middleware を Array#reverse_each していく
- Rack::Server#app
- ここでは options[:config] = confing.ru の前提で進めると、Rack::Builder.parse_file に config.ru を渡す
- Rack::Server#build_up
- Rack::Server#build_up(Rack::Server#app) を実行している
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
- Rack::Builder#run
- config.ru 中に
run <Rack middleware オブジェクト>
という DSL があるときに呼ばれる - @run に app を代入する
- config.ru 中に
- Rack::Builder#to_app