スポンサーリンク

2015年9月15日

[Elixir]GenServerの基本を習得する

とある錬金術師の万能薬(Elixir)

Goal

GenServerの基本を習得する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5

Wait a minute

プロセスやAgentをやってからしばらく経ちました。
忘れつつありますね。
と言うわけで、そろそろGenServerをやりましょう。
(文脈は犠牲になったのだ…)

Index

GenServer
|> Before you start
|> What is GenServer?
|> Check Function
|> Management of name
|> Extra

Before you start

Agentの記事で作ったプロジェクトを使います。
まだ実施していない方は、以前の記事を行って下さい。(コピペでもいいですが(笑))
基本的なAgentの使い方を習得する

What is GenServer?

GenServerって何ですか?
Getting Startを読んでもよく分からんかった。
なので、hexdocsのドキュメントを読みに行く。
GenServerは、
クライアント-サーバ関係のサーバを実現するためのbehaviourモジュールです。
ElixirとOTPで一般的なサーバを構築するため、抽象化されています。
GenServerはプロセスであり、他のElixirプロセスとして、
状態を維持、非同期などのコードを実行するため使用することができます。
汎用サーバプロセス(GenServer)を使用することの利点としては、
このモジュールを使用すると、インタフェース機能の標準セットを持っていると言うことです。
また、トレース及びエラー報告のための機能が含まれていることです。
GenServerには、2つの機能が実装されている。
  • クライアントAPI
  • サーバのコールバック

Check Function

頭の中が???状態なので、各機能に関してドキュメントを見てみる。
ドキュメントの方に凄く簡単なサンプルがあるので、まずこれを実行してみる。

Example:

サーバ側のコールバックを実装。
defmodule Stack do
  use GenServer

  def handle_call(:pop, _from, [h|t]) do
    {:reply, h, t}
  end

  def handle_cast({:push, item}, state) do
    {:noreply, [item|state]}
  end
end

Result:

実行結果。
iex> {:ok, pid} = GenServer.start_link(Stack, [:hoge])
{:ok, #PID<0.91.0>}
iex> GenServer.call(pid, :pop)
:hoge
iex> GenServer.cast(pid, {:push, :huge})
:ok
iex> GenServer.call(pid, :pop)
:huge
続き…クラッシュさせてみた。
関数にマッチしないとか言われてる。
iex> GenServer.call(pid, :pop)
** (EXIT from #PID<0.89.0>) an exception was raised:
    ** (FunctionClauseError) no function clause matching in Stack.handle_call/3
        (kv) lib/gen_server_example.ex:4: Stack.handle_call(:pop, {#PID<0.89.0>, #Reference<0.0.0.542>}, [])
        (stdlib) gen_server.erl:607: :gen_server.try_handle_call/4
        (stdlib) gen_server.erl:639: :gen_server.handle_msg/5
        (stdlib) proc_lib.erl:237: :proc_lib.init_p_do_apply/3

Interactive Elixir (1.0.5) - press Ctrl+C to exit (type h() ENTER for help)

14:47:53.085 [error] GenServer #PID<0.91.0> terminating
Last message: :pop
State: []
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in Stack.handle_call/3
        (kv) lib/gen_server_example.ex:4: Stack.handle_call(:pop, {#PID<0.89.0>, #Reference<0.0.0.542>}, [])
        (stdlib) gen_server.erl:607: :gen_server.try_handle_call/4
        (stdlib) gen_server.erl:639: :gen_server.handle_msg/5
        (stdlib) proc_lib.erl:237: :proc_lib.init_p_do_apply/3
iex>
クラッシュして -> すぐ復帰…なんだこれ。
すごいですね…Elixirを壊すコードとか、さっぱり思いつかない。
他の言語でも思いつきませんが(笑)
本気でそれをやるなら、ErlangVMを壊すことを考えないといけない気がしますが…
閑話休題…それぞれの関数を確認してみる。

Client API

今回使ってるクライアントAPIの関数は…
  • start_link/3
  • call/3
  • cast/2
GenServerの開始。現在のプロセスにリンクプロセスを開始する。
多くの場合、監視ツリーの一部としてGenServerを開始するために使用される。
サーバが起動されると、それを初期化するために与えられた引数を渡して、
特定のモジュール内のinit/1関数(コールバック)を呼び出す。
(init/1が戻るまで、同期始動手順を確実にするため、この関数は戻らない)

- call/3

callは、同期した要求を送信する。サーバの応答かタイムアウトを待つ必要がある。
(call ↔ handle_call)

- cast/2

castは、非同期の要求を送信する。サーバの応答は送信されない。
(cast -> handle_cast)
この関数は、宛先ノードまたはサーバが存在しないに関係なく、すぐに:okが返される。
但し、アトムとしてサーバが指定されない限り。
クライアントAPIは他にもあるが…今回使っているものだけ書いています。

6 callbacks

定義できるコールバックは以下の6つ…
  • init/1
  • handle_call/3
  • handle_cast/2
  • handle_info/2
  • terminate/2
  • code_change/3

- init/1

サーバの起動時に呼び出される初期化用のコールバック。

- handle_call/3

同期要求に使用する、デフォルトの選択肢。

- handle_cast/2

非同期要求に使用する、応答を求めない時。

- handle_info/2

send/2でのメッセージ、call/2、cast/2を介して送信されないサーバが受信することができる全てのメッセージで使用する。

- terminate/2

サーバが終了しようとしている時に呼び出される。
クリーンアップに有用とのこと。
戻り値に:okを返す必要がある。

- code_change/3

ホット・コード・スワップ。
アプリケーションコードをライブアップグレードする時に呼び出される。
(Phoenixのライブリロードで使われてそうな気がする…)
それぞれのコールバックにおける戻り値は、ドキュメントを参照して下さい。

Note:

send/2で送信されたメッセージで予期しないメッセージがサーバへ到着する可能性がある。
それらのメッセージでクラッシュさせたい場合、Supervisorが必要になる。
予期しないメッセージが不具合を引き起こす可能性は割と高いとのこと。

Management of name

elixir-langのGetting Startにある、GenServerのサンプルを作成してみる。

Example:

サンプルソースを作成します。
defmodule KV.Registry do
  use GenServer

  ## Client API

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  def lookup(server, name) do
    GenServer.call(server, {:lookup, name})
  end

  def create(server, name) do
    GenServer.cast(server, {:create, name})
  end

  def stop(server) do
    GenServer.call(server, :stop)
  end

  ## Server Callbacks

  def init(:ok) do
    names = HashDict.new
    refs = HashDict.new
    {:ok, {names, refs}}
  end

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

  def handle_call(:stop, _from, state) do
    {:stop, :normal, :ok, state}
  end

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

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

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

Example:

実行するためのテストを作成します。
defmodule KV.RegistryTest do
  use ExUnit.Case, async: true

  setup do
    {:ok, registry} = KV.Registry.start_link
    {:ok, registry: registry}
  end

  test "spawns buckets", %{registry: registry} do
    assert KV.Registry.lookup(registry, "shopping") == :error

    KV.Registry.create(registry, "shopping")
    assert {:ok, bucket} = KV.Registry.lookup(registry, "shopping")

    KV.Bucket.put(bucket, "milk", 1)
    assert KV.Bucket.get(bucket, "milk") == 1
  end

  test "removes buckets on exit", %{registry: registry} do
    KV.Registry.create(registry, "shopping")
    {:ok, bucket} = KV.Registry.lookup(registry, "shopping")
    Agent.stop(bucket)
    assert KV.Registry.lookup(registry, "shopping") == :error
  end
end
init/3は、{:ok, state}を返している。
handle_call/3は、{:reply, reply, new_state}を返している。
handle_cast/2は、{:noreply, new_state}を返している。
lookup/2とcreate/2の関数はサーバへ要求を送信する必要がある。
要求は、handle_call/3かhandle_cast/2の最初の引数で指定する。
複数の引数を提供するため、大体の場合はタプルで指定する。
例えば…
GenServer.call/2でサーバへ要求を送信している。
handle_call/3でサーバ側の応答を行っている。
そして、call/2の第二引数とhandle_call/3の第一引数は同じ。
def lookup(server, name) do
  GenServer.call(server, {:lookup, name})
end
def handle_call({:lookup, name}, _from, names) do
  {:reply, HashDict.fetch(names, name), names}
end

Extra

プロセスIDをモニタリングしてみる。

Example:

spawnで生み出したpidをモニタリングする。
iex> pid = spawn fn -> "hoge" end
#PID<0.94.0>
iex> Process.monitor(pid)
#Reference<0.0.0.569>
Agentで生み出したpidをモニタリングする。
iex> {:ok, pid} = Agent.start_link(fn -> HashDict.new end)
{:ok, #PID<0.91.0>}
iex> Process.monitor(pid)
#Reference<0.0.0.564>

Note:

自分でプロセスを生み出すのは、あまり良いことではないとのこと。
ならどうするのかと言うと、Supervisorを使うとのこと。
はよ、Supervisorやれってことですね。分かりました。

Speaking to oneself

すごい曖昧には分かった気がする…多分、ある程度は使える。
でも、モヤモヤ感が頭の中に渦巻いてる。
この言葉に言い表せない微妙な感じ…よくあることだな。
マクロを最初にやった時もこんな感じだった…大丈夫使っていれば覚えるさ~
とりあえず、Elixir in ActionにProcessで実装(?)しているGenServerっぽいもののソースコードがある。
まだ理解が追いつかないので、これも実践して試してみる。
Elixir in ActionのソースコードはGithubにアップされているみたいです。
各章毎にソースコードが管理されているようなので、クローンすればローカルでも勉強できます。
参考: Github - sasa1977/elixir-in-action
しかし、これ…一歩踏み込むとすぐErlangが出てくるな。
Erlangは文法さえ怪しいレベルです…ソースコードが読めない。

Bibliography

人気の投稿