123
121

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elixir の OTP (GenServer 編)

Last updated at Posted at 2015-07-23

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

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

もとい

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

あたりを解説してみる。

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

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

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

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

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

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

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

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

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 サーバーはメッセージを受け取って結果をクライアントにまたメッセージで返信している。間違ったクライアントにメッセージを送ってしまわないように

    send :kvs, {self(), q}

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

    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 を使って書き換える。

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/3handle_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 と繋がってることがわかって面白いと思う。

123
121
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
123
121

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?