Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: リクエストのライフサイクルとRackを理解する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。画像は元記事の引用です。

  • 2019/10/03: 初版公開
  • 2022/08/02: 更新

参考

以下のサイトで、Railsのリクエストの全ライフサイクルをビジュアル表示で追うことができます。本記事と合わせて参照することで理解が進むと思います。


rails-trace.chriszetter.comより

Rails: リクエストのライフサイクルとRackを理解する(翻訳)

本記事は、私達がRailsConf 2019で行ったスピーチのまとめです。スライドはこちらでご覧いただけます。

Example Rails controller code

エディタでコントローラのファイルを開き、アクションメソッドにRubyコードを少々書いて、ブラウザでURLを叩けば、書いたコードは即動き出します。ほとんどのRails開発者は、こうしたワークフローについてそこそこ慣れ親しんでいるはずです。しかし、このしくみを深く考えてみたことはありますか?ブラウザのアドレスバーにURLを入力してからコントローラのメソッドが呼び出されるまでのしくみを説明できますか?そのメソッドを実際に呼び出しているのがどこかご存知でしょうか?

インターネッツの旅路

'Let's meet for lunch' text conversion

ランチに誰かを誘おうとしている状況を考えてみましょう。「Pastiniでどう?」というメッセージは同僚に送信するには十分ですが、その地域に不案内な知人には少々不親切です。普通なら図のようにレストランの住所も送ってあげるべきでしょう。これでタクシーに行き先を伝えるなりマップで調べるなりできるようになります。

コンピュータでもこれと似たようなことが言えます。ブラウザにドメイン名だけを入力した場合、ブラウザの最初の仕事はサーバーに接続することです。「skylight.io」のようなドメイン名は人間には覚えやすくても、コンピュータはそれだけではサーバーを見つけられません。サーバーへの到達方法を知るには、そうしたドメイン名をコンピュータネットワークのアドレス、すなわちIPアドレス(Internet Protocolアドレスの略です、念のため)に変換する必要があります。

うすうすお気づきかと思いますが、IPアドレスは34.194.84.73のような感じになります。コンピュータはこうしたIPアドレスを用いることで、インターネット上で互いに接続されたネットワークをたどって目的のサーバーにたどり着けるようになります。

DNS(ドメインネームシステム)は、コンピュータがドメイン名をIPアドレスに変換して正しいサーバーを見つけるためのものです。皆さんのコンピュータでdigというユーティリティですぐに試せます(なおオンライン版のdigもあります)

; <<>> DiG 9.10.6 <<>> skylight.io
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32689
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;skylight.io.           IN  A

;; ANSWER SECTION:
skylight.io.        59  IN  A   34.194.84.73

;; Query time: 34 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Mon Apr 29 14:50:34 PDT 2019
;; MSG SIZE  rcvd: 56

digの出力は何だか難しそうに見えますが、重要なのは「ANSWER SECTION」です。ここではskylight.io34.194.84.73というIPアドレスに解決されています。

DNSは、IPアドレスに対応付けられたドメイン名を登録する場所です。ドメインを購入または所有した場合は、この対応付けを自分で行わなければなりません。そうしないと、顧客はあなたのサーバーを見つけられません。

サーバーのIPアドレスがわかれば、ブラウザからサーバーに接続できるようになります。ブラウザからサーバーへの接続方法はなかなか興味深いものです。両者の接続は、受話器を手に取って相手の電話番号に電話をかけているようなものと考えることが可能です。

実はこの部分についても、telnetと呼ばれるツールで実際に「生の」接続を試すことができます。たとえば、telnet 34.194.84.73 80と入力することで、先ほどのサーバーへの接続をオープンにできます。80はデフォルトのHTTPポート番号です。

訳注

telnetはmacOSやLinuxの多くのディストリビューションにはデフォルトでインストールされていません(telnetは通信を暗号化できません)。

参考: telnetコマンド | Linux技術者認定試験 リナック | LPI-Japan

An example telnet session

接続に成功したら、何かメッセージを伝えなければなりませんが、どんなメッセージを伝えればよいのでしょうか?ブラウザとサーバーが相手のメッセージを理解するには、互いに「話す」言葉について合意が取れていなければなりません。その言葉こそがHTTP(Hyper Text Transfer Protocl)と呼ばれるものです。ブラウザとサーバーは、どちらもHTTPを理解できます。

ここで行える最もシンプルなリクエストとして「skylight.io/hello」を送るには、「リクエストの種類: GET」「対象ホスト: skylight.io」「対象パス: /hello」を指定します。

GET /hello HTTP/1.1
Host: skylight.io

上をtelnetセッションに正しく入力すれば(リクエストの終わりを示すために、最後に空行を入力するのが肝心です)、サーバーから以下のようなレスポンスを受け取ります。

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
Date: Thu, 25 Apr 2019 18:52:54 GMT

Hello World

上は、リクエストが成功したことを示しています。いくつかのヘッダー情報に続いて、コントローラでレンダリングした「Hello World」というテキストが最後に表示されています。

HTTPは(バイナリプロトコルではない)平文テキストでできたプロトコルなので、人間が読んで理解することもデバッグすることもできます。HTTPは、ブラウザがWebページやアセットをリクエストしたり、フォームを送信したり、キャッシュを操作したり、圧縮したりといったさまざまな操作を体系的に提供します。

このブラウザとサーバーの間の通信は、一般の電話と同様、暗号化なしの状態です。

このリクエストは、相手側に届くまでにさまざまな場所を通過します。このカンファレンスのWi-Fiルーター、会場のコンベンションセンターのルーター、インターネットプロバイダ、サーバーをホスティングしている企業など、リクエストが正しい場所に届くまでに多くの中間ネットワークを経由します。つまり、その途中ではブラウザとサーバーのやりとりを盗み聞きする機会はいくらでもあるということです。しかし皆さんはもちろん盗み聞きなどされたくありませんよね?

ご心配なく。やりとりの内容は暗号化(encrypt)できます。暗号化されたやりとりも暗号化されていないやりとりとすべて同じところを通過するので、「やりとりが行われている」こと自体は中間地点にいてもわかります。しかし途中にいる者がやりとりの内容を理解することはできません。メッセージを復号(decrypt)するキーは、やりとりしているサーバーとブラウザしか持っていないからです。

これがHTTPSと呼ばれるプロトコルです(SはセキュアのSです)。HTTPSは、HTTPの単なる別種ではありません。上述のように平文テキストでやりとりをする点については同じですが、ブラウザはメッセージを送信する前にメッセージを暗号化し、サーバーはそれを復号してからメッセージを解釈します。

暗号化と復号は、ブラウザとサーバーの両方が秘密鍵(secret key)を用いて行います。ブラウザとサーバーの両者が暗号化通信に合意すれば、他の誰も内容を読むことはできません。なお、通信経路のあらゆる中間地点で通信を見ることができる状況で、ブラウザとサーバーが暗号化と復号に使う鍵を、鍵を奪われずにどうやって受け渡すのでしょうか?これは高度なトピックなので、別の機会にお話したいと思います。

サーバー

Logos of web servers

さて、ブラウザが無事サーバーに接続して、特定のWebページをサーバーにリクエストするところまで成功しました。このリクエストに対するレスポンスはどのように生成されるのでしょうか?

そもそもこれは何のサーバーなのかといえば、「Webサーバー」です。Webサーバーは、上述のとおり「HTTPを話す」サーバーです。apachenginxpassengerlighthttpdunicornpuma、果てはwebrickなどがWebサーバーの例です。Rubyで書かれたWebサーバーもありますし、C言語などで書かれたWebサーバーもあります。

Webサーバーの役割は、リクエストを「理解する」ことと、そのリクエストに対してどんなサービスを提供するかを決定することです。静的アセットを求めるシンプルなリクエストであれば、Webサーバーをそれ用に構成するだけで簡単に扱えるようになります。

たとえば、ブラウザが/assets/以下にあるものをリクエストしたら、常にアプリの/public/assetsフォルダ以下のものを返すようにWebサーバーを設定したいとします。リクエストされたアセットが存在すれば、サーバーはそれを圧縮して返し、存在しない場合は「404 not found」ページを返すべきです。

利用しているWebサーバーによっては、そのサーバー固有の設定言語や設定構文も使えたりします。たとえばNginxを使っていれば、nginx.confに以下のような記述があるでしょう。

location /assets {
  alias /var/myapp/public/assets;
  gzip_static on;
  gzip on;
  expires max;
  add_header Cache-Control public;
}

What about my blog?

しかしさらに凝ったことをやろうとすると、話は複雑になってきます。

たとえば、「ブラウザで/blogにアクセスしたら、直近のブログ記事10件をデータベースから取得して見た目を整え、コメントも表示し、HTMLヘッダーやフッターやナビゲーションバーを追加し、JavaScriptやCSSも追加してからレスポンスを返せ」とWebサーバーに指示したいとしましょう。

ここでWebサーバーの設定言語について解説しようとすると話がややこしくなってしまいます。しかし私たちにはRailsという味方がいるのですから、実際にWebサーバーに指示したいのは「リクエストを受け取ったら、そこから先はRailsに渡してお任せしろ」ということなのです。ところでWebサーバーとRailsはどのようにやりとりしているのでしょうか?

A few possible ways for the web server to communicate with Rails

Rubyでは、この種の情報をやりとりするための手法がいろいろ考えられます。RailsがブロックにWebサーバーを登録する方法もあれば、WebサーバーがRailsのメソッドを呼び出す方法もあります。Webサーバーはリクエストの情報をメソッドの引数という形で渡すことも、環境変数で渡すこともできます。グローバル変数で渡すことすら可能でしょう。さて、WebサーバーからRailsにリクエストの情報を渡したとき、そこにどのようなオブジェクトが存在するべきでしょうか?言い換えれば、RailsはどのようにしてWebサーバーに返信するのでしょうか?

最終的には上のどの方法も利用可能ではありますが、重要なのは、やりとりの両側で同じ規約に合意することです。Rackはそのために誕生しました。Rackは、WebサーバーがRuby製Webフレームワークとやりとりする(あるいはその逆)ための統一APIを提供するミドルウェアです。どんなRubyフレームワークであっても、Rackプロトコルを実装してRackの規約に沿っていれば、上述のさまざまなWebサーバーとシームレスにやりとりできるようになります。

The web server talking to Rails

Rackは、ごくわずかな機能を実現するためのシンプルなRubyプロトコルであり規約です。WebサーバーはWebフレームワークに「そちらの担当分のリクエストが1件来たぞ、そうそう、パスとかHTTP verbとかヘッダーもあるのでよろしく」と通知する必要があります。WebフレームワークはWebサーバーに「ほいきた、こちらの処理は終わったので結果(ステータスコード、ヘッダー、body)をお返しするぞ」と返信する必要があります。

Rackはこうしたやりとりを軽量かつフレームワーク非依存に扱うため、Rubyでできる最もシンプルな方法を選択します。RackはWebフレームワークへの通知をメソッド呼び出しで行い、やりとりの詳細はメソッドの引数を経由させ、戻り値をメソッド呼び出しから返すことでWebフレームワークに返信します。

以上をコードで表すと次のような感じになります。

env = {
  'REQUEST_METHOD' => 'GET',
  'PATH_INFO' => '/hello',
  'HTTP_HOST' => 'skylight.io',
  # ...
}

status, headers, body = app.call(env)

status  # => 200
headers # => { 'Content-Type' => 'text/plain' }
body    # => ['Hello World']

最初に、Webサーバーはハッシュを1つ用意します。これは「envハッシュ」と呼ばれるのが通例です。envハッシュにはHTTPリクエストの情報がすべて含まれています。たとえばREQUEST_METHODにはHTTP verbが、PATH_INFOにはリクエストのパスが、HTTP_*には対応するヘッダーの値が含まれます。

一方、アプリやフレームワークは#callメソッドを1つ実装しなければなりません。Webサーバーは、envハッシュを唯一の引数としてこのメソッドを呼び出します。ここでは、envハッシュ内の情報に基づいてリクエストが処理されることと、正確に3つの要素を含むarray(「3つのタプル」とも言います)を1つ返すことが期待されます。

この3つの要素とは何だかおわかりでしょうか?

第1の要素はHTTPステータスコードです。200ならリクエストの成功を、404ならnot foundを表すといった具合です。

第2の要素は、Content-Typeなどのレスポンスヘッダーを含む1個のハッシュです。

最後に第3の要素は、レスポンスのbodyとなる1個の配列です。このbodyはきっと文字列だろうとお思いの方もいるかもしれませんが、実は違います!いくつかの技術的な理由によって、このbodyは「each可能なオブジェクト」、つまり文字列をyieldするeachを実装しているオブジェクトなのです。シンプルな場合は、その中に文字列が1切れ入ったarrayを1個だけ返します。

Let's build a Rack app!

では実際に動くところをお目にかけましょう!Railsでやる前に、シンプルなRackアプリを1つ手作りしてみます。

# app.rb

class HelloWorld
  def call(env)
    if env['PATH_INFO'] == '/hello'
      [200, {'Content-Type' => 'text/plain'}, ['Hello World']]
    else
      [404, {'Content-Type' => 'text/plain'}, ['Not Found']]
    end
  end
end

これは、おそらく最もシンプルに構築できるRackアプリでしょう。継承をまったく使っておらず、#callを実装するシンプルなクラスが1つあるだけです。このコードはenvハッシュのリクエストパスを参照し、パスが正確に/helloとマッチすれば"Hello World"という平文レスポンスをレンダリングし、マッチしない場合は404 "Not Found"エラーレスポンスを出力します。

さてアプリができましたが、このアプリをどう使えばよいのでしょうか?アプリに仕事をさせるにはどうしたらよいのでしょうか?Rackは、Webサーバーが実装できるプロトコルの1つに過ぎないことを思い出しましょう。つまりこのアプリを、Rackの言葉でやりとりできるWebサーバーと接続する必要があります。

ありがたいことに、rackという便利なgemがあります。これはRackの仕様を実装するお便利ユーティリティ集で、rackupと呼ばれるサンプルWebサーバーを備えています。rackupはconfig.ruと呼ばれる形式の設定ファイルを認識します。

# config.ru

require_relative 'app'

run HelloWorld.new

config.ruは基本的にRubyファイルですが、若干の設定DSLを含んでいます。ここでは先ほどのappファイルをrequire_relativeし、Hello Worldアプリのインスタンスを構成して、runというDSLメソッドでrackupサーバーに渡しています。

これで、config.ruファイルのあるディレクトリでrackupコマンドを実行して、サーバーをデフォルトの9292番ポートにアタッチできるようになりました。http://localhost:9292/helloを開けば「Hello World」と表示され、http://localhost:9292/watあたりを開けば「Not Found」エラーが表示されます。

今度は、http://localhost:9292/というルートパスからhttp://localhost:9292/helloへのリダイレクトを追加したいとしましょう。この場合は次のようにアプリを変更できます。

# app.rb

class HelloWorld
  def call(env)
    if env['PATH_INFO'] == '/'
      [301, {'Location' => '/hello'}, []]
    elsif env['PATH_INFO'] == '/hello'
      [200, {'Content-Type' => 'text/plain'}, ['Hello World']]
    else
      [404, {'Content-Type' => 'text/plain'}, ['Not Found']]
    end
  end
end

これでも一応動きますが、このままでは今後の拡張が面倒になります。ここに継ぎ足していくとなると、ifelsifelseendが延々チェインしていくことになります。リダイレクトはよく行われる操作でもあるので、アプリの他の場所でも使い回しが効くようにしたいものです。リダイレクト機能の実装をモジュラーかつ再利用可能かつコンポジション可能にできるとよさそうですね。

もちろんできます!

# app.rb

class Redirect
  def initialize(app, from:, to:)
    @app = app
    @from = from
    @to = to
  end

  def call(env)
    if env["PATH_INFO"] == @from
      [301, {"Location" => @to}, []]
    else
      @app.call(env)
    end
  end
end

class HelloWorld
  def call(env)
    if env["PATH_INFO"] == '/hello'
      [200, {"Content-Type" => "text/plain"}, ["Hello World!"]]
    else
      [404, {"Content-Type" => "text/plain"}, ["Not Found!"]]
    end
  end
end

これでHelloWorldクラスの部分を一切変更せずに済むようになりました。その代りに、単一の責務をこなす役割を担うRedirectというクラスを新たに追加しました。マッチするパスがあればリダイレクトレスポンスを発行して終了し、ない場合は、このクラスに渡しておいた次のアプリに委譲します。

config.ruを以下のように変更して、リダイレクトが効くようにします。

require_relative 'app'

run Redirect.new(
  HelloWorld.new,
  from: '/',
  to: '/hello'
)

上はHelloWorldアプリのインスタンスを構成し、それをRedirectアプリに渡します。

これでRackミドルウェアを実装できました!ミドルウェアはRackの仕様の技術的な一部ではありません。Webサーバー目線では、ここにはRedirectというアプリがひとつあるきりです。このアプリの#callメソッドの中でさらに別のRackアプリが呼ばれますが、Webサーバーはそのことについて関知する必要はありません。

このミドルウェアのパターンはよく使われますので、config.ruにはそれ用のDSLキーワードがあります。

require_relative 'app'

use Redirect, from: '/', to: '/hello'

run HelloWorld.new

useキーワードを使えばネストがきれいさっぱりなくなります。嬉しいですね!

このミドルウェアパターンはきわめて強力です。余分なコードを書かなくても、rack gemのミドルウェアを数個追加するだけで、圧縮機能やHTTPキャッシュやHEADリクエストのハンドリングをアプリに追加できます。

require_relative 'app'

use Rack::Deflater
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag

use Redirect, from: '/', to: '/hello'

run HelloWorld.new

このようにしてとても機能的なアプリを構築できることがおわかりかと思います。

Rails 3より前のRack

ついにRailsまでたどり着きました!

言うまでもなく、RailsはRackを実装しています。自分のRailsアプリを覗いてみれば、以下の記述があるconfig.ruファイルが見つかるはずです。

require_relative 'config/environment'

run Rails.application

config.ruはrackup由来ではありますが、その他のWebサーバーやHerokuなどのサービスでも認識されるので、config.ruがデフォルトでRailsに含まれているのは便利です。

runキーワードには何らかのRackアプリを渡すことになっています。つまりRails.application#callに応答するRackアプリでもあるはずです!Railsコンソールで試してみましょう。

Trying it in the Rails console

仕様に沿ったenvハッシュをわざわざ手作りしなくても、rack gemのRack::MockRequest.env_forユーティリティメソッドでできます。このメソッドにURLをひとつ渡せば後は代わりにやってくれます。このRails.application.call呼び出しにこのenvハッシュを渡せば、ステータスコードとヘッダーとbodyが期待どおりタプル(tuple)として生成されます。見慣れたリクエストログをコンソールに出力することもできます。素晴らしい!

ところで、このRailsアプリのconfig.ruにはどこにもuseステートメントがありません。Railsはミドルウェアを全然使っていないのでしょうか?そんなことはありません。実は、アプリに入っている全ミドルウェアを以下のコマンド一発でおなじみのconfig.ru構文の形で表示できます。

$ bin/rails middleware

use Rack::Sendfile
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Blorgh::Application.routes

これを見れば、Railsではcookieハンドリングなどの多くの機能を「ミドルウェア」の形で実装していることがわかります。これはうまい設計です。なにしろAPIサーバーを実装する場合は、不要なミドルウェアを削除するだけで済むのですから。ところでどうやってミドルウェアを削除するのでしょうか?

useステートメントは、単にconfig.ruでアプリのビルドを便利にするためのものであることを思い出しましょう。Webサーバーはアプリの最も外側の部分にしか注目していないのです。Railsのconfig/application.rbにも、ミドルウェアを管理するのに便利なものがあります。

# config/application.rb

require_relative 'boot'
require 'rails/all'

Bundler.require(*Rails.groups)

module Blorgh
  class Application < Rails::Application

    # cookieを無効にする
    config.middleware.delete ActionDispatch::Cookies
    config.middleware.delete ActionDispatch::Session::CookieStore
    config.middleware.delete ActionDispatch::Flash

    # 独自のミドルウェアを追加する
    config.middleware.use CaptchaEverywhere

  end
end

ついにアプリまでたどり着いた!

ここまでミドルウェアについて見てきましたが、肝心のアプリはどうなったのでしょう?bin/rails middlewareの出力を見ると、useがミドルウェア用、runがアプリ用とわかるので、どうやらBlorgh::Application.routesがそれに違いありません!

Our app!

Railsコンソールで同じテストを実行すると、Rails.application.callが無事Blorgh::Application.routesに置き換わっているかどうかを確認できます。すべて問題なく動いています。だとすると、このRackアプリとは一体何なのでしょう?そしてこのRackアプリはどこから来たのでしょう?

このRackアプリはリクエストURLに着目し、膨大なルーティング規則とマッチさせて、呼び出すべき正しいコントローラとアクションを検索します。Railsはこのconfig/routes.rbに基づいて、アプリを生成してくれます。

# config/routes.rb

Rails.application.routes.draw do
  resources :posts
end

ほとんどのRails開発者はresources DSLを既にご存知でしょう。resourcesは、多くのルールを一度に定義するショートハンドです。resourcesは最終的に7つのルーティングに展開されます。

# config/routes.rb

Rails.application.routes.draw do
  # resources :postsから以下ができる
  get '/posts' => 'posts#index'
  get '/posts/new' => 'posts#new'
  post '/posts' => 'posts#create'
  get '/posts/:id' => 'posts#show'
  get '/posts/:id/edit' => 'posts#edit'
  put '/posts/:id' => 'posts#update'
  delete '/posts/:id' => 'posts#destroy'
end

たとえばGETリクエストを/postsに送信するとPostsController#indexメソッドが呼び出され、PUTリクエストを/posts/:idに送信するとPostsController#updateメソッドが呼び出されます。

ところでこのposts#indexという文字列は一体何なのでしょう?PostsControllerindexアクションだということぐらいはわかります。Railsでこのコードを追ってみると、最終的にPostsController.action(:index)という形に展開されます。さてこれは一体?

Action Controllerのコードをうんとシンプルにしたものを以下に示します。

class ActionController::Base
  def self.action(name)
    ->(env) {
      request = ActionDispatch::Request.new(env)
      response = ActionDispatch::Response.new(request)
      controller = self.new(request, response)
      controller.process_action(name)
      response.to_a
    }
  end

  attr_reader :request, :response, :params

  def initialize(request, response)
    @request = request
    @response = response
    @params = request.params
  end

  def process_action(name)
    event = 'process_action.action_controller'

    payload = {
      controller: self.class.name,
      action: action_name,
      # ...
    }

    ActiveSupport::Notifications.instrument(event, payload) do
      self.send(name)
    end
  end
end

上の冒頭にはactionクラスメソッドがあり、lambda(->)を返すことがわかります。このlambdaはenvと呼ばれる引数を1つ受け取ります。このenvは何かというと、驚かないでください、ハッシュなのです!そしてlambdaが返すのは何とarrayなのです!さらにこのlambdaは#callに応答するのです!つまりこれがRackアプリです。隅から隅までRackアプリなのです。

最後にここまでの話をまとめて考えると、Rackアプリは以下のような姿のルーティングアプリであることが想像できるでしょう。

class BlorghRoutes
  def call(env)
    verb = env['REQUEST_METHOD']
    path = env['PATH_INFO']

    if verb == 'GET' && path == '/posts'
      PostsController.action(:index).call(env)
    elsif verb == 'GET' && path == '/posts/new'
      PostsController.action(:new).call(env)
    elsif verb == 'POST' && path == '/posts'
      PostsController.action(:create).call(env)
    elsif verb == 'GET' && path =~ %r(/posts/.+)
      PostsController.action(:show).call(env)
    elsif verb == 'GET' && path =~ %r(/posts/.+/edit)
      PostsController.action(:edit).call(env)
    elsif verb == 'PUT' && path =~ %r(/posts)
      PostsController.action(:update).call(env)
    elsif verb == 'DELETE' && path = %r(/posts/.+)
      PostsController.action(:destroy).call(env)
    else
      [404, {'Content-Type': 'text-plain', ...}, ['Not Found!']]
    end
  end
end

上のコードは、渡されたリクエストパスとHTTP verbを、自分のルーティング設定で定義されたルールとマッチさせ、コントローラで適切なRackアプリに委譲します。こういうコードを手作りしなくて済むのは、ひとえにRailsのおかげです!

ところで、Railsはこの対応付けを行うために、どうやってこれをルーティング設定から生成し、すべてのリクエストを効率よくルーティングするのでしょうか?これについては昨年のRailsConfでちょうどこの話題を扱ったセッションがあります↓ので、どうぞご覧ください。

これで「すべてはRackアプリである」ことがわかりました。私たちはこれらを自由に「選んで組み合わせられる」のです1。ここでプロならではの技😆をいくつかお目にかけましょう。

1. Railsアプリの一部からRackアプリへのルーティングを次のように書けることをご存知ですか?

Rails.application.routes.draw do
  get '/hello' => HelloWorld.new
end

2. 私たちは既にlambdaについて学んだので、以下のようにインラインで書くことも可能です。

Rails.application.routes.draw do
  get '/hello' => ->(env) {
    [200, {'Content-Type': 'text/plain'}, ['Hello World!']
  }
end

3. ルーターのredirect DSLは何をしていると思いますか?驚かないでください!実はRackアプリを返すのです。「信じられない」と思う人もいるかもしれませんが、もう皆さんも先ほどこの機能を使っています。

Rails.application.routes.draw do
  # redirect(...) はRackアプリを返す!
  get '/' => redirect('/hello')
end

4. その気になれば、Railsアプリの中でSinatraアプリをマウントすることすら可能です。まさかと思う人もいるかもしれませんが、SidekiqのWeb UIはSinatraで書かれています。つまり皆さんもRailsアプリの中でSinatraアプリを動かしたことがあるかもしれないのです。

Rails.application.routes.draw do
  mount Sidekiq::Web, at: '/sidekiq'
end

原文追記

Sidekiq 4.2以降のWeb UIは、外部依存性を削減するためにカスタムフレームワークに移行しました。もちろん相変わらずRackプロトコルを使っています。

この移行のプルリクは、今回Rackプロトコルについて学んだことを元に最小版のSinatraをビルドするのに必要なものを学ぶうえで、興味深い事例です(特に基本的なルーティングの取扱、ビューのレンダリング、リダイレクトなど)。

5. もちろん、逆にSinatraアプリの中でRailsアプリをマウントすることも可能ですが、ここは皆さんのご想像にお任せしましょう。

ここで学んだことを応用すれば、controller#actionという文字列を以下のように置き換えることすらできます。

Rails.application.routes.draw do
  get '/posts' => PostsController.action(:index)
  get '/posts/new' => PostsController.action(:new)
  post '/posts' => PostsController.action(:create)
  get '/posts/:id' => PostsController.action(:show)
  get '/posts/:id/edit' => PostsController.action(:edit)
  put '/posts/:id' => PostsController.action(:update)
  delete '/posts/:id' => PostsController.action(:destroy)
end

その気になれば、bin/rails middlewareの出力をconfig.ruファイルにコピペすることもできます。

なお、実際には皆さんのRailsアプリでこういうやんちゃをするのはおすすめしません。そんなことをしたら、オートローディングや一部のパフォーマンス最適化がバイパスされてgemでミドルウェアを足すこともできなくなり、Rails周りで将来変更が発生したときにつらくなるだけです。とはいえ、このようにしてすべてが組み立てられていることを理解するのは実にクールですよね。

Example Rails controller code

そういうわけで、やっとのことで出発地点であるコントローラアクションまで帰ってきました。「ところでrender plain...が、Rack仕様で要求されるレスポンスのタプルになるまでの道のりの話はどうなったのか?」今回は残念ながら時間がありませんが、続きは「The Lifecycle of a Rails Response」的なセッションかブログ記事を出すと思いますのでご期待ください。

お知らせ: Skylightのしくみ

本記事では、フレームワークが魔法でも何でもないことを学んできました。フレームワークはあくまで単なるレイヤの1つであり、統一的に十分定義されたプリミティブの上にそのレイヤがトッピングされたものに過ぎません。こうした規約(convention)を学ぶことは、Railsの使い方やあなたのスキルを他の開発者と共有するうえで有用ですが、何よりも、規約はコミュニティのすべての人々にツールを書く力を与えてくれます。

たとえば、私たちSkylightではリクエストの実行全体に要した時間を測定する手段を必要としています。ミドルウェアよりも効果的に測定をやれるのはどんな方法でしょうか?

$ bin/rails middleware

use Skylight::Middleware
use Rack::Sendfile
use ActionDispatch::Executor
...
run Blorgh::Application.routes

「設定より規約」は、単にRailsアプリを構築する方法論にとどまりません。Railsの規約があることで、Skylightは独自の設定を書かなくても顧客のWebアプリの詳細な情報を集められるのです。

当初、私たちはRailsがアクションをどのようにディスパッチしているかをActionController::Base#process_actionをシンプルにしたもので調べました。このメソッドの内部では、Railsがアクションをコントローラにディスパッチするときに組み込みのActiveSupport::Notificationsというinstrumentation(測定)システムを用いて、何らかの事象が発生していることをSkylightなどのライブラリに通知します。SkylightはこのAPIによって、顧客のWebアプリのエンドポイント名を設定なしで取得します。

A condensed Skylight trace

Skylightでは平均レスポンス時間以外の情報も提供できるので、顧客のWebアプリのリクエスト全体について集約された情報をお届けします。私たちはActiveSupport::NotificationsやRailsコミュニティの規約を駆使して、RailsのテンプレートレンダリングやActive Recordの実行タイミングはもちろん、著名なHTTPライブラリやライブラリのキャッシュ状況、Mongoidなど他のデータベースライブラリについても、Rails規約に沿った詳細情報を提供します。

デフォルトでは顧客のWebアプリのリクエストの重要な部分のみを表示しますので、何が起こっているかを急いで調べることに集中できます。この例であれば、エンドポイントを高速化するにはおそらくAppSerializerを集中的にチェックするべきでしょう。Skylightは、顧客がより多くの情報を表示する必要が生じれば、顧客のWebアプリで用いられているすべてのRackミドルウェアなどのさまざまな情報も収集します。

An expanded Skylight trace

以上は、Skylightが提供するサービスの氷山の一角に過ぎません。ご興味がおありでしたら、こちらの採用情報をご覧ください!

おたより発掘

関連記事

Rails: レスポンスのライフサイクルとETagを理解する(翻訳)

Webアプリの基礎とさまざまな実行環境を理解する#1(社内勉強会)


  1. 訳注:「MIX & Match」は韓国ドラマのタイトルです。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。