Goal
GenServerの基本を習得する。
Dev-Environment
OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5
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
|> What is GenServer?
|> Check Function
|> Management of name
|> Extra
Before you start
What is GenServer?
GenServerって何ですか?
Getting Startを読んでもよく分からんかった。
なので、hexdocsのドキュメントを読みに行く。
なので、hexdocsのドキュメントを読みに行く。
GenServerは、
クライアント-サーバ関係のサーバを実現するためのbehaviourモジュールです。
ElixirとOTPで一般的なサーバを構築するため、抽象化されています。
クライアント-サーバ関係のサーバを実現するための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
- start_link/3
GenServerの開始。現在のプロセスにリンクプロセスを開始する。
多くの場合、監視ツリーの一部としてGenServerを開始するために使用される。
多くの場合、監視ツリーの一部としてGenServerを開始するために使用される。
サーバが起動されると、それを初期化するために与えられた引数を渡して、
特定のモジュール内のinit/1関数(コールバック)を呼び出す。
(init/1が戻るまで、同期始動手順を確実にするため、この関数は戻らない)
特定のモジュール内のinit/1関数(コールバック)を呼び出す。
(init/1が戻るまで、同期始動手順を確実にするため、この関数は戻らない)
- call/3
callは、同期した要求を送信する。サーバの応答かタイムアウトを待つ必要がある。
(call ↔ handle_call)
(call ↔ handle_call)
- cast/2
castは、非同期の要求を送信する。サーバの応答は送信されない。
(cast -> handle_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のライブリロードで使われてそうな気がする…)
アプリケーションコードをライブアップグレードする時に呼び出される。
(Phoenixのライブリロードで使われてそうな気がする…)
それぞれのコールバックにおける戻り値は、ドキュメントを参照して下さい。
Note:
send/2で送信されたメッセージで予期しないメッセージがサーバへ到着する可能性がある。
それらのメッセージでクラッシュさせたい場合、Supervisorが必要になる。
予期しないメッセージが不具合を引き起こす可能性は割と高いとのこと。
それらのメッセージでクラッシュさせたい場合、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}を返している。
handle_call/3は、{:reply, reply, new_state}を返している。
handle_cast/2は、{:noreply, new_state}を返している。
lookup/2とcreate/2の関数はサーバへ要求を送信する必要がある。
要求は、handle_call/3かhandle_cast/2の最初の引数で指定する。
要求は、handle_call/3かhandle_cast/2の最初の引数で指定する。
複数の引数を提供するため、大体の場合はタプルで指定する。
例えば…
GenServer.call/2でサーバへ要求を送信している。
handle_call/3でサーバ側の応答を行っている。
そして、call/2の第二引数とhandle_call/3の第一引数は同じ。
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を使うとのこと。
はよ、Supervisorやれってことですね。分かりました。
Speaking to oneself
すごい曖昧には分かった気がする…多分、ある程度は使える。
でも、モヤモヤ感が頭の中に渦巻いてる。
でも、モヤモヤ感が頭の中に渦巻いてる。
この言葉に言い表せない微妙な感じ…よくあることだな。
マクロを最初にやった時もこんな感じだった…大丈夫使っていれば覚えるさ~
マクロを最初にやった時もこんな感じだった…大丈夫使っていれば覚えるさ~
とりあえず、Elixir in ActionにProcessで実装(?)しているGenServerっぽいもののソースコードがある。
まだ理解が追いつかないので、これも実践して試してみる。
まだ理解が追いつかないので、これも実践して試してみる。
Elixir in ActionのソースコードはGithubにアップされているみたいです。
各章毎にソースコードが管理されているようなので、クローンすればローカルでも勉強できます。
参考: Github - sasa1977/elixir-in-action
各章毎にソースコードが管理されているようなので、クローンすればローカルでも勉強できます。
参考: Github - sasa1977/elixir-in-action
しかし、これ…一歩踏み込むとすぐErlangが出てくるな。
Erlangは文法さえ怪しいレベルです…ソースコードが読めない。
Erlangは文法さえ怪しいレベルです…ソースコードが読めない。