この記事は先日の記事の続きとなります.
プロセスでエラーが発生したら, 何が起こるのか?
試しに実際にエラーを起こしてみましょう
これまで作成してきたモジュールに, 次のような関数を追加して..
def error() do
GenServer.call(@name, :error)
end
def handle_call(:error, _client, state) do
raise "Something Error!!"
end
実行して見ると, こうなります.
iex(2)> RemoteAgent3.start "init message"
:ok
iex(3)> RemoteAgent3.get
"init message"
iex(4)> RemoteAgent3.error
** (EXIT from #PID<0.58.0>) an exception was raised:
** (RuntimeError) Something Error!!
remote_agent3.ex:30: RemoteAgent3.handle_call/3
(stdlib) gen_server.erl:629: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:661: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Interactive Elixir (1.3.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
13:39:03.056 [error] GenServer :agent_register3 terminating
** (RuntimeError) Something Error!!
remote_agent3.ex:30: RemoteAgent3.handle_call/3
(stdlib) gen_server.erl:629: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:661: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: :error
State: "init message"
iex(1)> RemoteAgent3.get
** (exit) exited in: GenServer.call(:agent_register3, :get, 5000)
** (EXIT) no process
(elixir) lib/gen_server.ex:564: GenServer.call/3
iex(1)>
このように, エラーが発生した後にもう一度サーバプロセスにアクセスしようとすると
** (EXIT) no process
とプロセスが存在しないというエラーが出るようになってしまいます.
しかし, この状態で改めてRemoteAgent3をstartしてみると...
iex(1)> RemoteAgent3.get
** (exit) exited in: GenServer.call(:agent_register3, :get, 5000)
** (EXIT) no process
(elixir) lib/gen_server.ex:564: GenServer.call/3
iex(1)> RemoteAgent3.start "restart message"
:ok
iex(2)> RemoteAgent3.get
"restart message"
元通り機能を使えるようになります.
このように, Elixirでは, プロセスでエラーが発生した場合, そのプロセスを生きのびさせようとはせず, いっぺんそのプロセスを完全に殺してしまい,
改めて新しいプロセスを立ち上げることで, 全体としての堅牢性を確保する設計となっています.
さて, プロセスが死んだら新しくプロセスを立ち上げ直すのはいいのですが, 一体誰がプロセスを立ち上げるのでしょうか?
上の例では, クライアントプロセスが立ち上げていますが, まさかサーバが落ちたらクライアントが立ち上げ直してくれなんていうわけにも行きません.
そこで出てくるのが, プロセスの監視と管理を行うSupervisorというビヘイビアです.
Supervisorを使ってみる
公式ドキュメントを真似しながら, RemoteAgentに対するSupervisorを作ってみましょう.
defmodule RemoteAgent.Supervisor do
use Supervisor
def start_link(message \\ "") do
Supervisor.start_link(__MODULE__, message)
end
def init(message) do
children = [
worker(RemoteAgent3, [message])
]
supervise(children, strategy: :one_for_one)
end
end
defmodule RemoteAgent3 do
use GenServer
@name :agent_register3
def get() do
GenServer.call(@name, :get)
end
def update(new_state) do
GenServer.cast(@name, {:update, new_state})
end
def error() do
GenServer.call(@name, :error)
end
# GenServer Callback functions
def start_link(state \\ "") do
GenServer.start_link(RemoteAgent3, state, name: @name)
end
def handle_call(:get, _client, state) do
{:reply, state, state}
end
def handle_call(:error, _client, state) do
raise "Something Error!!"
end
def handle_cast({:update, new_state}, state) do
{:noreply, new_state}
end
end
supervisorでは, プロセス立ち上げ時にCallbackされるinit関数内で, RemoteAgent3を監視対象とします.
def init(message) do
children = [
worker(RemoteAgent3, message)
]
supervise(children, strategy: :one_for_one)
end
また, RemoteAgent3モジュールについても, プロセス立ち上げの為の関数を
独自のstart関数からGenServerビヘイビアで定義されているstart_link関数に変更しました.
動作を確認してみましょう.
iex(3)> RemoteAgent.Supervisor.start_link "init"
RemoteAgent.Supervisor.start_link "init"
{:ok, #PID<0.72.0>}
iex(4)> RemoteAgent3.get
RemoteAgent3.get
"init"
iex(5)> RemoteAgent3.update "updated"
RemoteAgent3.update "updated"
:ok
iex(6)> RemoteAgent3.get
RemoteAgent3.get
"updated"
iex(7)> RemoteAgent.error
RemoteAgent.error
** (UndefinedFunctionError) undefined function RemoteAgent.error/0
RemoteAgent.error()
iex(7)> RemoteAgent3.error
RemoteAgent3.error
** (exit) exited in: GenServer.call(:agent_register3, :error, 5000)
** (EXIT) an exception was raised:
** (RuntimeError) Something Error!!
/home/vagrant/my_cli/lib/remote_agent3.ex:28: RemoteAgent3.handle_call/3
(stdlib) gen_server.erl:629: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:661: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
14:04:35.582 [error] GenServer :agent_register3 terminating
** (RuntimeError) Something Error!!
/home/vagrant/my_cli/lib/remote_agent3.ex:28: RemoteAgent3.handle_call/3
(stdlib) gen_server.erl:629: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:661: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: :error
State: "updated"
(elixir) lib/gen_server.ex:564: GenServer.call/3
iex(7)> RemoteAgent3.get
RemoteAgent3.get
"init"
見ての通り, 一度エラーが起きた後, プロセスが自動的に立ち上がり, RemoteAgent3.get
関数を呼び出してみても何事もなかったように関数が実行されていることがわかります.
プロセスの状態を復元する
ここまでで, プロセスの再スタートが自動的に行われるようにはなりましたが, プロセス再スタート後の状態が初期状態に戻ってしまっています.
iex(7)> RemoteAgent3.get
RemoteAgent3.get
"init"
このままでは問題なので, プロセスの死亡時の状態を何処かに保存しておき, 再スタート時に状態を復帰できるようにしましょう.
状態の保存の仕方は色々あると思われますが, ここでは単純にAgentを使うことにします.
まず, プロセス立ち上げ時の引数を値そのものからagentのpidに変えます.
def start_link(agent) do
GenServer.start_link(RemoteAgent3, agent, name: @name)
end
次に, プロセス立ち上げ時に一度だけ呼ばれるCallBackであるinit関数を追加し, Agentから状態を取得するようにします.
def init(agent) do
state = Agent.get agent, &(&1)
{:ok, {agent, state}}
end
そして, handle_call
とhandle_cast
で扱う状態を, 単一の値から, {agent_id, 状態}
というタプルに変えて
def handle_call(:get, _client, {agent, state}) do
{:reply, state, {agent, state}}
end
def handle_call(:error, _client, {_agent, _state}) do
raise "Something Error!!"
end
def handle_cast({:update, new_state}, {agent, _state}) do
{:noreply, {agent, new_state}}
end
最後に, プロセス終了時にCallbackされるterminate関数を追加し, 状態をAgentに退避するようにします.
def terminate(_reason, {agent, state}) do
Agent.update agent, fn _n -> state end
end
コード全体はこんな感じになります.
defmodule RemoteAgent3 do
use GenServer
@name :agent_register3
def get() do
GenServer.call(@name, :get)
end
def update(new_state) do
GenServer.cast(@name, {:update, new_state})
end
def error() do
GenServer.call(@name, :error)
end
# GenServer Callback functions
def start_link(agent) do
GenServer.start_link(RemoteAgent3, agent, name: @name)
end
def init(agent) do
state = Agent.get agent, &(&1)
{:ok, {agent, state}}
end
def handle_call(:get, _client, {agent, state}) do
{:reply, state, {agent, state}}
end
def handle_call(:error, _client, {_agent, _state}) do
raise "Something Error!!"
end
def handle_cast({:update, new_state}, {agent, _state}) do
{:noreply, {agent, new_state}}
end
def terminate(_reason, {agent, state}) do
Agent.update agent, fn _n -> state end
end
end
defmodule RemoteAgent.Supervisor do
use Supervisor
def start_link(message \\ "") do
Supervisor.start_link(__MODULE__, message)
end
def init(message) do
{:ok, agent} = Agent.start(fn -> message end)
children = [
worker(RemoteAgent3, [agent])
]
supervise(children, strategy: :one_for_one)
end
end
動作を確認してみると...
iex(3)> RemoteAgent.Supervisor.start_link "this is init message"
{:ok, #PID<0.72.0>}
iex(4)> RemoteAgent3.get
"this is init message"
iex(5)> RemoteAgent3.update "this is updated message"
:ok
iex(6)> RemoteAgent3.get
"this is updated message"
iex(7)> RemoteAgent3.error
** (exit) exited in: GenServer.call(:agent_register3, :error, 5000)
** (EXIT) an exception was raised:
** (RuntimeError) Something Error!!
/home/vagrant/my_cli/lib/remote_agent3.ex:33: RemoteAgent3.handle_call/3
(stdlib) gen_server.erl:629: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:661: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
16:58:47.430 [error] GenServer :agent_register3 terminating
** (RuntimeError) Something Error!!
/home/vagrant/my_cli/lib/remote_agent3.ex:33: RemoteAgent3.handle_call/3
(stdlib) gen_server.erl:629: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:661: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: :error
State: {#PID<0.73.0>, "this is updated message"}
(elixir) lib/gen_server.ex:564: GenServer.call/3
iex(7)> RemoteAgent3.get
"this is updated message"
無事, プロセスの状態の復元に成功しました.
まとめ
親プロセスが子プロセスを監視し, 必要に応じて廃棄したり立ち上げたり様子が何となくわかっていただけたかと思います.
ここでの例では, 単純に監視する側とされる側だけの単純な2階層しかありませんでしたが,
実際のアプリケーションでは更にSupervisorを監視するSupervisorや, そのSupervisorを監視するSupervisor…
と何階層にも渡る複雑な構造が作られていき, 更にはそのアプリケーション(つまり階層構造の最上位プロセス)を監視し機能を利用する上位アプリケーションが作られているようです.
(Ectoアプリケーションを利用する, PhoenixFrameworkアプリケーションみたいな関係)
最小単位であるプロセスから, より大きな単位であるアプリケーションまで, どの部分を切り出してもすべて同じ構造をしているというのは, 実に美しい設計といえるんじゃないでしょうか.
参考
Elixir.Supervisor: http://elixir-lang.org/docs/stable/elixir/Supervisor.html