前回に続き、Elixirで遠隔PCに侵入風 のパラレルワールド的展開をいきます
今回の内容、上記スライドによる社内勉強会をやっていたときから、スライドに入れるべきか、モヤモヤしてた内容なんだけど、tokyo.exのおーはらさんの記事の勢いに元気付けられて、展開した内容なんですよ
catサーバ/クライアントもGenServer化する
前回 作ったpwdサーバは、今回お話したい条件を満たさないため、スライドP32にある「catサーバ/クライアント」をGenServer化することから始めます
defmodule PassGenServer do
use GenServer
def handle_call( :pwd, _from, _state ) do
{ :ok, result } = File.cwd()
{ :reply, result, "" }
end
def handle_call( { :cat, path }, _from, _state ) do
{ :ok, result } = File.read( path )
{ :reply, result, "" }
end
end
Elixirプロジェクトフォルダ直下に、a.txtというファイルを適当に用意した後、実行します
iex> recompile()
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
iex> GenServer.call( pid, { :cat, "a.txt" } )
"I'm a.txt"
うまくいきましたね
では、存在しないファイルをcatすると、どうなるでしょう?
iex> GenServer.call( pid, { :cat, "b.txt" } )
** (EXIT from #PID<0.206.0>) an exception was raised:
** (MatchError) no match of right hand side value: {:error, :enoent}
(node1) lib/pass_genserver.ex:10: PassGenServer.handle_call/3
(stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:647: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
14:31:55.520 [error] GenServer #PID<0.211.0> terminating
** (MatchError) no match of right hand side value: {:error, :enoent}
(node1) lib/pass_genserver.ex:10: PassGenServer.handle_call/3
(stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:647: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:cat, "b.txt"}
エラーになりました
ここから、a.txtをcatしてみると...
iex> GenServer.call( pid, { :cat, "a.txt" } )
warning: variable "pid" does not exist and is being expanded to "pid()", please use parentheses to remove the ambiguity or change the variable name
iex:1
** (CompileError) iex:1: undefined function pid/0
(stdlib) lists.erl:1354: :lists.mapfoldl/3
pidが参照できない状態になっています
これは、上記エラー発生時に、サーバがダウンしてしまったことが原因なので、再度サーバ起動してやり直すと...
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
iex> GenServer.call( pid, { :cat, "a.txt" } )
"I'm a.txt"
復活しました
さて通常、こういったケースでは、File.read()を「try ~ catch」で囲むような例外処理を入れて対応したりしますが、これをやり始めると、途端にコードが複雑で見通し悪くなっていきます
また、エラー処理に落とし込むべきところを、誤って例外スルーしてしまう、といったバグも作り込みがちです
別のやり方として、プロセスを監視して、ダウンしたら自動的に再起動する、というアプローチもありますが、これもまた面倒です
Elixirには、本質的な処理と、エラー処理を分離するために、「OTPスーパーバイザ」という仕組みがあり、こうした問題をシンプルかつクリーンに解決できます
OTPスーパーバイザで例外処理
OTPスーパーバイザを使うための準備として、start_link()を定義し、その中からプロセス起動のための処理として、GenServer.start_link()の呼出を入れます
defmodule PassGenServer do
use GenServer
def start_link( state, opts ) do
IO.puts( "--- PassGenServer.start_link( #{inspect( state )}, #{inspect( opts ) } ) ---" )
GenServer.start_link( __MODULE__, state, opts )
end
def handle_call( :pwd, _from, _state ) do
{ :ok, result } = File.cwd()
{ :reply, result, "" }
end
def handle_call( { :cat, path }, _from, _state ) do
{ :ok, result } = File.read( path )
{ :reply, result, "" }
end
end
では、OTPスーパーバイザを使ってみましょう
スーパーバイザ経由で、GenServerを起動することで、GenServerをスーパーバイザ管理下に置くことができます
なお、「import Supervisor.Spec」は、スーパーバイザ用ヘルパーを使うためのimportです
iex> import Supervisor.Spec
iex> servers = [ worker( PassGenServer, [ 0, [ name: :server_process ] ] ) ]
iex> Supervisor.start_link( servers, strategy: :one_for_one )
サーバの呼出は、GenServer時とほぼ同じです(pidがプロセス名である「:server_process」に変わった程度です)
iex> GenServer.call( :server_process, { :cat, "a.txt" } )
"I'm a.txt"
ここまではうまくいきました
では、存在しないファイルをcatしてみます
iex> GenServer.call( :server_process, { :cat, "b.txt" } )
** (MatchError) no match of right hand side value: {:error, :enoent}
(node1) lib/pass_genserver.ex:15: PassGenServer.handle_call/3
(stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:647: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:cat, "b.txt"}
State: ""
--- PassGenServer.start_link( 1, [name: :server_process] ) ---
** (exit) exited in: GenServer.call(:server_process, {:cat, "b.txt"}, 5000)
** (EXIT) an exception was raised:
** (MatchError) no match of right hand side value: {:error, :enoent}
(node1) lib/pass_genserver.ex:15: PassGenServer.handle_call/3
(stdlib) gen_server.erl:615: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:647: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
(elixir) lib/gen_server.ex:737: GenServer.call/3
エラーは出ますが、サーバは自動的に再起動されていますので、a.txtをcatしてみると...
iex> GenServer.call( :server_process, { :cat, "a.txt" } )
"I'm a.txt"
今度は大丈夫でした
こんな感じで、start_link()の定義だけすれば、プロセスダウンをリカバリーしてくれる「OTPスーパーバイザ」、相当シンプルなのに「やりおるわ」という感じです
あれ?大丈夫なんだっけ?
さて、ここでお察しの良い方は、以下4つの疑問を持ったのでは無いかと思います
- 気軽にプロセスを再起動したけど、負荷とか大丈夫なの?
- 負荷は大丈夫だとしても、プロセスが処理していた状態やデータとかどうなるの?
- 地味に、cdサーバと一緒にpwdサーバもダウンしてるけど、障害切り分けとして、マズくない?
- 「strategy: :one_for_one」というコードの説明が無かったけど、これ一体、何?
こういった疑問に、以降でお答えしていきますね
(次回に続く)