Elixir の OTP (GenServer 編)


[[翻訳] ElixirにおけるOTPの紹介](http://qiita.com/HirofumiTamori/items/b72ad232dc3b26e87a03) という記事が上がってた (いつも翻訳ありがとうございます) のでそれに触発されて勢いで書いてみる。

ちなみに今後発売される Web+DB PRESS Vol.88, 89 の連載では2回続けて Elixir の記事を書いているよ。

もとい

- Elixir 状態を扱うにはどうするのが定番か
- 軽量プロセスを使ったクライアント/サーバーのありがちな実装
- それを GenServer を使うとどう書けるのか
- GenServer 使うとどんないいことがあるのか

あたりを解説してみる。

## Elixir のプロセスで状態を扱う

Elixir で状態を扱いたい場合は軽量プロセスの仕組みを使って、プロセスの中に状態を閉じ込めて、そのプロセスとのメッセージパッシングで状態への操作を行うのが定石である。

その例として、例えば key + value を保存できるプロセスを作ってみる。

- KVS という名前で実装を作る
- KVS には `store/2` と `lookup/1` という API を持たせて、それぞれ key, value の保存と取得を行う
- `store/2` と `lookup/1` は実際はプロセスとのメッセージパッシングのラッパ
- プロセスは、プロセス辞書 (プロセスに固有の dict) にとりあえず値を保存する

という方針で、まずは作ってみる。

KVS の API としてはこんな感じになる。

```ex
KVS.start
KVS.store(:weather, :raining)
IO.inspect KVS.lookup(:weather) # :raining が返る

```

以下、その実装。(ちょっと記憶があやふやだけどプログラミングErlangに載ってた例を Elixir で書き換えたものだったきがする)

```ex
defmodule KVS do
  def start do
    pid = spawn_link(fn -> loop() end)
    Process.register pid, :kvs
  end

  def store(key, value) do
    rpc {:store, key, value}
  end

  def lookup(key) do
    rpc {:lookup, key}
  end

  def rpc(q) do
    send :kvs, {self(), q}
    receive do
      {:kvs, reply} -> reply
    end
  end
  
  def loop do
    receive do
      {from, {:store, key, value}} -> 
        Process.put(key, {:ok, value})
        send from, {:kvs, true}
        loop()
      {from, {:lookup, key}} ->
        send from, {:kvs, Process.get(key)}
        loop()
    end
  end
end

```

`start/0` が呼ばれると spawn_link/1 でプロセスが立ち上がり、イベントループの `loop/0` に入る。`loop/0` は再帰になっていて、クライアントからのメッセージを `receive/0` し続ける。

`reveive/0` ではクライアントからのメッセージの内容に合わせて、値を保存するのか、取得するのかを判断する。

こんな具合である。特に難しいところはないと思う。

なお、この KVS サーバーはメッセージを受け取って結果をクライアントにまたメッセージで返信している。間違ったクライアントにメッセージを送ってしまわないように

```ex
    send :kvs, {self(), q}
```

と、クライアントは自分の pid (`self()`) をつけてメッセージを送り、サーバー側は

```ex
    receive do
      {from, {:store, key, value}} -> 
        Process.put(key, {:ok, value})
        send from, {:kvs, true}
```

と、その受け取りもと (`from`) に対して返信を行っている。

## OTP ビヘイビア ─ GenServer

この、プロセスに状態をもたせてメッセージパッシングでやりとりする、みたいなのは Erlang や Elixir でしょっちゅう実装する。つまり、パターンである。

Erlang/Elixir においてはその手の頻出パターンを「ビヘイビア」と呼んで形式化している。形式化? まあようするに、そのパターンを実装するためのフレームワークとかインタフェースを用意してくれてるってこと。

今回の KVS のようなクライアント/サーバー実装は GenServer がそのビヘイビアモジュールである。

先の実装を GenServer を使って書き換える。

```ex
defmodule KVS.Server do
  use GenServer

  def store(key, value) do
    GenServer.cast(:kvs, {:store, key, value})
  end

  def lookup(key) do
    GenServer.call(:kvs, {:lookup, key})
  end

  # GenServer が実装を要求してくるコールバック
  def start_link do
    GenServer.start_link(__MODULE__, HashDict.new, name: :kvs)
  end

  def handle_cast({:store, key, value}, state) do
    {:noreply, Dict.put(state, key, value)}
  end

  def handle_call({:lookup, key}, _from, state) do
    {:reply, state[key], state}
  end
end
```

以下のように使う。

```
KVS.Server.start_link
KVS.Server.store(:weather, :fine)
IO.inspect KVS.Server.lookup(:weather)

```

`receive/0` しつづける実装とか、クライアントからの pid を受け取ってそいつに `send` するとかその辺の記述が一切なくなっている。GenServer は他の言語でいうところの抽象クラスみたいなもの。receive とか send とかその辺は GenServer 側でやるから、自分とこの処理は `handle_cast/3` や `handle_call/3` という形でコールバックで実装しろや、というものである。

それらのコールバックでは戻り値のデータ構造にも決まりがある。`{:reply, state[key], state}` とかね。このタプルの2番目の要素がクライアントへのメッセージで、3番目の要素がこのプロセスが扱ってる状態。

なお、先の実装では状態の扱いに プロセス辞書を使っていたが、ここでは HashDict を引き回すことでそれを達成している。

## ビヘイビアに乗っかったおかげで ─ Supervisor

GenServer を使うと、こんな感じでサーバーの実装を端折れる･･･というのもあるんだけど重要なのはそこというよりは次で、ビヘイビアに従っておけば、そのビヘイビアを前提にしてる他の部品の恩恵に与れる、ということである。

代表的なのが Supervisor。Erlang や Elixir では、プロセスがエラーになったらどうすんべ、という問題に対して「プロセス自身ががんばってそれを維持しようとするんでなく、プロセスは構わずクラッシュして死んでしまえ。その代わり他のプロセスでそいつを監視して、再起動するとかエラー戦略練ってやるから」という方針で対処するのが良いアプローチとされる。Supervisor は、その監視する側の機能を実装してくれてるモジュールである。

先の KVS.Server を Supervisor で監視させてみよう。

Mix でプロジェクトを作る時に `--sup` オプションを付けると Supervisor を使うアプリケーションとしてプロジェクトを初期化できる。

```
$ mix new --sup --module=KVS kvs
```

KVS.Server はそのまま lib/kvs/server.ex に置く。

そして、lib/kvs.ex ･･･ これは `--sup` オプションを付けると自動で生成されてるんだけど、これの children に KVS.Server　を指定する。

```lib/kvs.ex
defmodule KVS do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      # ここ追加
      worker(KVS.Server, [])
    ]

    opts = [strategy: :one_for_one, name: KVS.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

```

これだけで OK。KVS.Server は一切弄らずに Supervisor から監視されるようになった。ビヘイビアに乗っかったおかげである。 
以下動作確認。

```
$ iex -S mix
iex(1)> KVS.Server.store(:weather, :file)
:ok
iex(2)> KVS.Server.lookup(:weather)
:file
```

先ほどの kvs.ex をみると

```
    opts = [strategy: :one_for_one, name: KVS.Supervisor]
    Supervisor.start_link(children, opts)
```

こんな感じで、監視した結果エラーになったらどうする? というのに対して戦略 (strategy) を指定している。`:one_for_one` は確か死んだら再起動しろ、ってことだったはず。他にも死んだら監視してるプロセス全部まとめて再起動とか、パラメータ変えて再起動とかいろんな戦略を指定することができる。

(そこまでは試すの面倒なのでやってないけど) なので、このサーバーをクラッシュさせても何事もなかったように生き返ります。

## まとめ

- 状態はプロセスで扱う
- その辺のパターンはビヘイビアとして形式化されててフレームワーク化されてる
- フレームワークに乗っかると Supervisor そのほかいろんな恩恵に与ることができる

ちなみに Phoenix で生成したコードを見てると、Phoenix もまたこの辺のビヘイビアに従って Supervisor や Cowboy と繋がってることがわかって面白いと思う。