Ruby on Rails のConditionalGet について

cuzic です。

**Ruby on Rails Advent Calendar** 24日目の記事です。

今日はクリスマスイブです。
実は生まれて初めての Advent Calendar への投稿です。
さらに Qiita で書いたのも初めてです。
ちょっとドキドキです。

今日は、Ruby on Rails における `Conditional Get` に
ついて書きます。

## ConditionalGet とは
ここで書こうとしている `ConditionalGet` とは、
`HTTP1.1` で規定されている取得済みのコンテンツであれば
ブラウザ内のローカルキャッシュを使わせることで、
トラフィック量を削減する機能のことです。

一般に、ブラウザは動作を高速化するため、トラフィックを
軽減するため、コンテンツを一旦キャッシュとして保管します。

とはいえ、キャッシュを使う限り、そのキャッシュは今でも
有効なのか、全文を再取得すべきなのかを判断する必要があります。

このキャッシュの有効性の判断はコンピュータ科学の２つの難問の１つです。

>  There are only two hard things in Computer Science:
>   cache invalidation and naming things.
> -- Phil Karlton

この難問に対する `HTTP` の回答が `Last-Modified` ヘッダと、
`ETag` ヘッダです。

サーバは応答するときにコンテンツの `Last-Modified` と
`ETag` を付与します。ブラウザはその値を覚えておきます。

同じコンテンツにアクセスするとき、 ブラウザは `ETag` の値を
`If-None-Match` ヘッダとしてサーバに送信します。
サーバでは前回の `ETag` と同一かどうか確認し、
同一であれば `304 Not Modified` という応答を返します。
このとき応答するのはヘッダのみでよいため、ネットワークトラフィックの
節約になります。

当然、この `ETag` は、本文のハッシュ値など異なるコンテンツであれば
異なる値になるようにする必要があります。

## Rack での ConditionalGet
### ConditionalGet が使われているかの確認方法
`Ruby on Rails` では `ConditionalGet` を下記の `Rack` ミドルウェアが
自動的に透過的に実行してくれます。

 - `Rack::ETag`
 - `Rack::ConditionalGet`

なお、自分の環境で、どのような Rack ミドルウェアが使われいるかは、
次のコマンドで確認できます。

```shell-session
 rake middleware
```

上記のコマンドを実行すると、 `Rack::ConditionalGet` と `Rack::ETag` は
かなり下の方にあるはずです。

これらの Rack ミドルウェアが下の方に位置しているのには理由があります。
その理由は、後ほど述べます。

### `Rack::ETag` と `Rack::ConditionalGet` の動作
`Rack::ETag` は、WEBアプリケーションサーバからの
レスポンスに対して、 `Etag` ヘッダを付与する `Rack` ミドルウェアです。

`Rack::ETag` は、下記の３つの条件がすべて満たされるとき、
`response.body` の `MD5` の値を `ETag` を付与します。

  - レスポンスのステータスコードが 200 か 201 であること
  - `to_path` メソッドに応答しないこと 　（※）
  - `Cache-Control` ヘッダが `no-cache` でないこと
  - `ETag` ヘッダや `Last-Modified` ヘッダがすでに含まれていないこと

（※） to_path メソッドに応答するときは、 `Apache` や `NginX` がレスポンスを処理するので、
ここで `ETag` を付与する必要がない。くわしくは、 `X-Sendfile` などで `Google` すること。

そして、 `ETag` がすでに含まれていることを前提として、
`Rack::ConditionalGet` が下記の動作を行います。

　1. ブラウザのキャッシュ内のコンテンツが `fresh` （最新と一致している）かどうかを判断する。
　2. fresh であれば次の処理を行う。
　　　応答ステータスを 200 から 304 に変更する
　　　Body 、Content-Type、Content-Length を削除する。

`fresh` かどうかの判定は、下記のように行います。

 * リクエストに `If-Modified-Since` ヘッダがあれば、それがレスポンスヘッダの`Last-Modified` よりも新しいこと。
 * リクエストに `If-None-Match` ヘッダがあれば、それがレスポンスヘッダの `ETag` と一致していること。

まとめると、開発者が、特になにもしていなかったとしても、
**ETag** を **Rack::ETag** が付与してくれています。
そして、 **Rack::ConditionalGet** が適切に 304 の応答を返します。

### Rack ミドルウェアの順序
Rack ミドルウェアは `rake middleware` で表示される順序に
従って処理が行われます。
リクエストを処理するときは上から順に処理され、
レスポンスを返すときは下から順に処理されます。

つまり、レスポンスを返すとき、 `Rack::ETag` がまず処理して、
`ETag` ヘッダを付与し、それに従って、 `Rack::ConditionalGet` が
適切に `304 not modified` の応答を返します。

`Rack::ConditionalGet` で 304 を返す場合、他の Rack ミドルウェア
での処理を行う必要もありません。
早い段階で `ConditionalGet` で応答することで処理を効率よく
することができます。

## Ruby on Rails での ConditionalGet
プログラマがたとえ特に何も指定しなくても、
`Rack::ConditionalGet` によって、適切に `304 Not Modified` を
応答する処理をしてくれます。

これは Rails アプリケーションによるレスポンスの生成処理が
すべて終わってから、Rack ミドルウェアが行うものです。
ネットワークトラフィックの節約にはつながります。
しかし、WEBアプリケーションサーバの CPU 負荷の低減にはなりません。

304 Not Modified を返すかどうかの判断をレスポンスの生成処理を行う
より早い段階で実行できれば、WEBアプリケーションサーバの
CPU 負荷の低減できます。

この目的のために `ActionController::ConditionalGet` モジュールの
`fresh_when` や `stale?` があります。

紙幅が限られているため、 `fresh_when` を使う例を簡単にだけ紹介します。

下記の例を見てみましょう。

```rb
 def show
   @article = Article.find(params[:id])
   fresh_when(@article)
 end
```

この簡単な例では、 `Last-Modified` に `@article.updated_at` メソッドの値を
`ETag` に `@article.cache_key` メソッドの値が使われます。

この簡単なコードによって、クライアントのキャッシュが `fresh` かどうかの
判定が行われ、 `fresh` であれば View のレンダリングをスキップし、
`304 Not Modified` を応答させることができます。

`cache_key` メソッドは知らない方もいるかもしれません。
そのレコードを一意に特定するための文字列を生成するメソッドで、
他にもフラグメントキャッシュを使うときなどにもこのメソッドは有用です。

## まとめ
今回紹介した `ConditionalGet` のテクニックは
`Ruby on Rails` の優れた特長を表しています。

第１の特長として、プログラマが特に何も意識することなく、
自動的に透過的にフレームワークが適切に処理してくれる点です。
ネットワーク負荷の低減のための `304 Not Modified` を応答する
ためのコードを一切書く必要がないのです。

第２の特長は、しかもそれをモジュール化された適用範囲の高い
技術によって実現している点です。この場合は、Rack ミドルウェア
という Rails に限らず適用可能な優れたモジュール化技術で実現されています。
Rack ミドルウェアで実現されているため、より高度な部品に交換することも
容易に可能となっています。

第３の特長は、デフォルトの動作に加えてさらに高度な処理をすることも
非常に簡単な点です。たとえば、プログラマがほんの少しのコードを
書き足すだけで、 `fresh` であれば View のレンダリング処理を
スキップさせることができます。