Elixir の OTP まわりの復習として、Phoenix でプロジェクトを作成した時に作られる Supervisor ツリーに GenServer で作った Worker も入れてみて、動作を確認してみました。
最終的なソースコードは GitHub のphoenix-studies-genserverにあります。
Phoenix のファイル準備
特に特別なところはありません。brunch も ecto も必要ないので、
mix phoenix.new study --no-brunch --no-ecto
で生成したものを使っています(※テスト環境の4000ポートは埋まっているので、config/dev.exs
の port は5000に変更してあります)。
Worker を GenServer で作成
lib/study/counter.ex
として、下記のようなモジュールを作成しました。
get_state/0 を呼び出すと状態の値を、inc/0 はカウントを+1, また kill/0 は該当する handle_call/2 が無いため、パターンマッチエラーになることを期待しています。
defmodule Study.Counter do
use GenServer
def start_link(), do: start_link(0)
def start_link(init_num) do
GenServer.start_link(__MODULE__, %{last_message: "初期状態", count: init_num}, name: __MODULE__)
end
def get_state do
GenServer.call(__MODULE__, :get_state)
end
def inc do
GenServer.cast(__MODULE__, :inc)
end
def kill do
GenServer.cast(__MODULE__, :kill)
end
####################
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
def handle_cast(:inc, state) do
new_state = %{count: state.count+1, last_message: "inc呼び出し"}
{:noreply, new_state}
end
end
動作テスト
iex -S mix
で、作成した GenServer が正しく動くことを確認します。
iex(1)> Study.Counter.start_link
{:ok, #PID<0.175.0>}
iex(2)> Study.Counter.get_state
%{count: 0, last_message: "初期状態"}
iex(3)> Study.Counter.inc
:ok
iex(4)> Study.Counter.get_state
%{count: 1, last_message: "inc呼び出し"}
iex(5)> Study.Counter.inc
:ok
iex(6)> Study.Counter.get_state
%{count: 2, last_message: "inc呼び出し"}
iex(7)> Study.Counter.kill
** (EXIT from #PID<0.173.0>) an exception was raised:
** (FunctionClauseError) no function clause matching in Study.Counter.handle_cast/2
(study) lib/study/counter.ex:28: Study.Counter.handle_cast(:kill, %{count: 2, last_message: "inc呼び出し"})
(stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:681: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> [error] GenServer Study.Counter terminating
** (FunctionClauseError) no function clause matching in Study.Counter.handle_cast/2
(study) lib/study/counter.ex:28: Study.Counter.handle_cast(:kill, %{count: 2, last_message: "inc呼び出し"})
(stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:681: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:"$gen_cast", :kill}
State: %{count: 2, last_message: "inc呼び出し"}
nil
iex(2)> Study.Counter.get_state
** (exit) exited in: GenServer.call(Study.Counter, :get_state, 5000)
** (EXIT) no process
(elixir) lib/gen_server.ex:596: GenServer.call/3
また、Study.Counter.kill がクラッシュすることや、クラッシュ後にプロセスが死んでいることも確認しています。
Supervisor の準備
作成した Worker をスーパーバイザ管理下にするため、lib/study.ex
の children の worker の値を下記のように変更します。[0] はカウントの初期値です。
children = [
# Start the endpoint when the application starts
supervisor(Study.Endpoint, []),
# Start your own worker by calling: Study.Worker.start_link(arg1, arg2, arg3)
# worker(Study.Worker, [arg1, arg2, arg3]),
worker(Study.Counter, [0]) # ← この行を追加
]
Supervisor 配下の Worker の再起動を確認
Supervisor により、Worker がクラッシュしても再起動されることを確認します。再度 iex -S mix
で試してみます。
最初の起動もスーパーバイザが自動的に行なってくれるため、Study.Counter.start_link/0 を実行すると :already_started
でエラーになることに注意してください(つまり、start_link を実行する必要は無い、ということです)。
iex(1)> Study.Counter.start_link
{:error, {:already_started, #PID<0.225.0>}}
iex(2)> Study.Counter.get_state
%{count: 0, last_message: "初期状態"}
iex(3)> Study.Counter.inc
:ok
iex(4)> Study.Counter.get_state
%{count: 1, last_message: "inc呼び出し"}
iex(5)> Study.Counter.kill
:ok
iex(6)> [error] GenServer Study.Counter terminating
** (FunctionClauseError) no function clause matching in Study.Counter.handle_cast/2
(study) lib/study/counter.ex:28: Study.Counter.handle_cast(:kill, %{count: 1, last_message: "inc呼び出し"})
(stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:681: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:"$gen_cast", :kill}
State: %{count: 1, last_message: "inc呼び出し"}
nil
iex(7)> Study.Counter.get_state
%{count: 0, last_message: "初期状態"}
Phoenix から呼び出し
Worker の動作、スーパーバイザによる再起動の動作確認ができましたので、Phoenix から Worker を呼び出してみます。と言っても、コントローラで関数を呼び出して、テンプレートにセットしているだけですが。
router.ex
に、Worker クラッシュ用の kill
を追加します。
scope "/", Study do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
get "/kill", PageController, :kill
end
controllers/page_controller.ex
および templates/page/index.html.eex
は以下です。
index では現在の状態を取得して inc/0 を呼び出しています。また kill はパターンマッチ該当無しでクラッシュさせて、/ にリダイレクトしています。
defmodule Study.PageController do
use Study.Web, :controller
def index(conn, _) do
params = Study.Counter.get_state
call_inc(Study.Counter)
render(conn, "index.html", params)
end
defp call_inc(process_name) do
process_name.inc
end
def kill(conn, _) do
Study.Counter.kill()
url = page_path(conn, :index)
redirect(conn, to: url)
end
end
<div class="jumbotron">
<h2><%= gettext "Welcome to %{name}", name: "Phoenix!" %></h2>
<p class="lead">A productive web framework that<br />does not compromise speed and maintainability.</p>
</div>
<div class="row marketing">
<div class="col-lg-6">
<h4>Resources</h4>
<ul>
<li> 最終メッセージ:<%= @last_message %>
<li> カウント数:<%= @count %>
</ul>
</div>
</div>
<a href="<%= page_path(@conn, :kill) %>">/kill</a>
mix phoenix.server
を実行して http://localhost:5000' に接続すると、Worker の状態の取得や inc/0 によるカウントが動くこと、また
http://localhost:5000/kill` に接続すると Worker がクラッシュして再起動されることを確認できます。
非常に簡単ですね!