Phoenix で Cache をする

Phoenix に Cache 機構をいれてみます。
今回は [ConCache](https://github.com/sasa1977/con_cache) を使って [OpenWeatherMap API](http://openweathermap.org/api) のレスポンスをキャッシュしてみます。

Phoenix アプリケーションでの外部 API リクエストについては[コチラ](http://qiita.com/mserizawa/items/3ac483aca4f2926fa1a9)を参照してください。


# Elixir におけるキャッシュについて

キャッシュ機構の実装というと、シングルトンクラスを作って処理することをイメージするかもしれませんが、これは *オブジェクト指向* な考え方であり、Elixir のような *プロセス指向* な言語には当てはめられません。
プロセス指向では、その名の通り **データをストアしておくプロセス** を作成して処理します。プロセスの作成と聞くとなにやら大変そうな気がしますが、Erlang/OTP のおかげでシンプルに実装できるようになっています。
では早速、ConCache を利用してプロセスを用いたキャッシュを体験してみましょう。

# 依存関係に ConCache を追加する

`cache_sample` というアプリケーションを作り、`mix.exs` に以下のように追記しましょう。

```bash
$ mix phoenix.new cache_sample
```

```ex:mix.exs
defmodule CacheSample.Mixfile do
  ...

  def application do
    [mod: {CacheSample, []},
     applications: [:phoenix, :phoenix_html, :cowboy, :logger,
                    :phoenix_ecto, :postgrex,
                    # ConCache を追記
                    :con_cache]]
  end

  ...

  defp deps do
     ...
     {:cowboy, "~> 1.0"},
     # HTTPoison を追記（API リクエスト用）
     {:httpoison, "~> 0.7.2"},
     # ConCache を追記
     {:con_cache, "~> 0.8.1"}]  end
end
```

最後にライブラリのダウンロードを行います。

```bash
$ mix deps.get
```

# プロセスを作成する

Erlang/OTP では、子プロセス（worker）は親プロセス（supervisor）に監視させる必要があります。
Phoenix ではこの設定が `lib/cache_sample.ex` で管理されていて、以下のように追記することで ConCache プロセスが作成できます。

```ex:cache_sample.ex
defmodule CacheSample do
  ...
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      supervisor(CacheSample.Endpoint, []),
      worker(CacheSample.Repo, []),
      # ConCache を追記
      worker(ConCache, [[], [name: :weather_cache]])
    ]
    ...
    opts = [strategy: :one_for_one, name: CacheSample.Supervisor]
    Supervisor.start_link(children, opts)
  end
  ...
end
```

children に ConCache プロセスを追加します。この場合は `:weather_cache` という名前のキャッシュが作成されます。
`Supervisor.start_link(children, opts)` という部分で親プロセスの監視下で各プロセスが立ち上げられています。

# API リクエスト部分を実装する

[コチラ](http://qiita.com/mserizawa/items/3ac483aca4f2926fa1a9)と同じ実装ですので、記載は割愛させていただきます。
以下のリクエストで天気データが返ってくれば OK です。

```bash
$ curl -s "http://localhost:4000/api/weather/35.6585373/139.7151887" | jq .
{
  "humidity": 74,
  "pressure": 1006,
  "temp": 26.99,
  "temp_max": 31.11,
  "temp_min": 24.44
}
```

# キャッシュ部分を実装する

本題のキャッシュ機構を実装しましょう。
今回 Key とする値は「緯度・経度」です。
ConCache には `get_or_store/3` という関数が用意されていて、これはその名の通り「指定した Key のキャッシュがあればそれを返却、なければ生成して追加 & 返却」というものです。そうそうこれが欲しかった、とはまさにこのことですね。

`page_controller.ex` のレスポンス生成部分を以下のように書き換えます。

```ex:page_controller.ex
defmodule CacheSample.PageController do
  ...
  def weather(conn, %{"lat" => lat, "lon" => lon}) do
    key = "#{lat},#{lon}"
    value = ConCache.get_or_store(:weather_cache, key, fn() ->
      HTTPoison.start
      result = HTTPoison.get! "http://api.openweathermap.org/data/2.5/weather?units=metric&lat=#{lat}&lon=#{lon}"

      case result do
        %{status_code: 200, body: body} -> Map.get(Poison.decode!(body), "main")
        %{status_code: code} -> %{error: code}
      end
    end)
    json conn, value
  end
end
```

仕組みは簡単で、最初に Key となる文字列を作成して、それを `get_or_store/3` で問い合わせます。
キャッシュされていなかったら、今まで通り OpenWeatherMap API に問い合わせを行い、結果の JSON をストアします。
最後に、`get_or_store/3` から返却された JSON を返却して完了です。

とても簡単ですね。

# 効果を測定する

curl コマンドは以下のようにオプションをつける事でレスポンス速度だけ出力されることができます。

```bash
$ curl -kL "http://www.example.com/" -o /dev/null -w "%{time_total}\n" 2> /dev/null
0.354
```

これを使って同じ緯度経度を用いたリクエストのレスポンス速度が向上されているかを確認してみましょう。

```bash
$ curl -kL "http://localhost:4000/api/weather/35.6995477/139.5769432" -o /dev/null -w "%{time_total}\n" 2> /dev/null
0.566
$ curl -kL "http://localhost:4000/api/weather/35.6995477/139.5769432" -o /dev/null -w "%{time_total}\n" 2> /dev/null
0.024
$ curl -kL "http://localhost:4000/api/weather/35.6995477/139.5769432" -o /dev/null -w "%{time_total}\n" 2> /dev/null
0.024
```

0.566 秒 -> 0.024 秒 となり、しっかりとキャッシュが効いていることが確認できました。

# 感想

* Erlang/OTP のおかげて、プロセスについてそんなに意識しなくても実装できるようになっている
* ConCache には関数や設定項目が豊富に用意されていて、かなり実用的
    * TTL（生存期間）もチューニングできるのは素晴らしい
* ただ、全キャッシュクリアのやり方だけイマイチ分からず...