OTPを勉強した(GenServer)

前のエントリでAgentを勉強したので、今回はGenServerの公式ガイドを読む。

以下のガイドをざっくりと訳しながらやってきます

前置き

Agentの場合プロセスのpidを特定するために、以下のように名前をつけることができる

iex(1)> Agent.start_link(fn -> %{} end, name: :shopping)
{:ok, #PID<0.82.0>}
iex(2)> Agent.update(:shopping, &Map.put(&1, :foo, "bar"))
:ok

但しこれには問題があって、elixirのprocess nameはatom型である必要があるが、このatom型はガベージコレクションの回収対象にならない。

したがってもしユーザーのinputによってprocess nameをつけ続けるとメモリが逼迫してしまいシステムがダウンしてしまう恐れが高い。

そしてそれを解決するために、名前とプロセスをマッピングをし監視するプロセスをGenServerで作る。

GenServerの実装

GenServerは

  • client API
  • server callback

といった2種類の実装によって成り立っている。

client APIが実際に使うインターフェースで、 server callbackがGenServerビヘイビアによって提供されている内部モジュール?といえば良いのだろうか。handle_callとかhandle_castとかがこれに当てはまる。

このcallbackをオーバーライドすることでインターフェースと内部実装の関係を疎にできるとともに、処理を柔軟に実装することが可能になっているぽい。

以下サンプルコード(ガイドのものほぼそのまま)

defmodule KV.Registry do
  use GenServer

  ## Client API

  @doc """
  Starts the registry
  """
  def start_link do
    GenServer.start_link(__MODULE__, :ok, [])
  end

  @doc """
  Looks up the bucket pid for `name` stored in `server`.

  Return `{:ok, pid}` if the bucket exists, `{:error}` otherwise.
  """
  def lookup(server, name) do
    GenServer.call(server, {:lookup, name})
  end

  @doc """
  Ensures there is bucket associated to the given `name` in `server`
  """
  def create(server, name) do
    GenServer.cast(server, {:create, name})
  end

  @doc """
  Stops the registry.
  """
  def stop(server) do
    GenServer.stop(server)
  end

  ## Server Callbacks
  def init(:ok) do
    names = %{}
    refs = %{}
    {:ok, {names, refs}}
  end

  def handle_call({:lookup, name}, _from, {names, _} = state) do
    {:reply, Map.fetch(names, name), state}
  end

  def handle_cast({:create, name}, {names, refs}) do
    if Map.has_key?(names, name) do
      {:noreply, {names, refs}}
    else
      {:ok, pid} = KV.Bucket.start_link
      ref = Process.monitor(pid)
      refs = Map.put(refs, ref, name)
      names = Map.put(names, name, pid)
      {:noreply, {names, refs}}
    end
  end

  def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do
    {name, refs} = Map.pop(refs, ref)
    names = Map.delete(names, name)
    {:noreply, {names, refs}}
  end

  def handle_info(_msg, state) do
    {:noreply, state}
  end
end

使うときは以下のようなかんじ

  1. {:ok, registry} = KV.Registry.start_linkで管理プロセスを立ち上げる
  2. KV.Registry.create(registry, "foo")で値を保持するためのバケットプロセスを立ち上げる
  3. {:ok, bucket} = KV.Registry.lookup(registry, "foo")でバケットプロセスのidを取得
  4. KV.Bucket.put(bucket, "hoge", "huga")でバケットプロセスにkey, valueを保存
  5. KV.Bucket.get(bucket, "hoge")でバケットプロセス内の値を取得

仮にAgent.stop/1等でバケットプロセスが停止、削除された場合にも、handle_info/2によってそのバケットプロセスは管理プロセスに残ること無く削除されるようになっている。

またhandle_info/2が2つ定義されているのはhandle_info/2がGenServer以外のメッセージも受けとるためで、管理プロセスがパターンマッチせずにエラーで停止してしまうという事態を防いでいる。

ちなみにサンプルコード中の__MODULE__は現在のmodule名。 すなわちこの場合はKV.Registryが返される。

所感

  • OTP上手く使えれば便利そう。上手く使えれば。。
  • elixirのドキュメント翻訳されてるの見たことないしやりたい

参考