Elixirで遠隔PCに侵入#3風「OTPスーパーバイザで例外処理をシンプルに作る」

[前回](http://qiita.com/piacere/items/49c44d7a450c0400de91)に続き、[Elixirで遠隔PCに侵入風](http://qiita.com/piacere/items/96398166f72fc2d921e9) のパラレルワールド的展開をいきます :relaxed:

[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/e570984a-7c6d-d316-df08-159e75bb9e2b.png)](https://www.slideshare.net/piacere_ex/elixir2pc-75571538)

今回の内容、上記スライドによる社内勉強会をやっていたときから、スライドに入れるべきか、モヤモヤしてた内容なんだけど、tokyo.exの[おーはらさんの記事](https://employment.en-japan.com/engineerhub/entry/2017/06/12/110000)の勢いに元気付けられて、展開した内容なんですよ :grin:

# catサーバ／クライアントもGenServer化する

[前回](http://qiita.com/piacere/items/49c44d7a450c0400de91) 作ったpwdサーバは、今回お話したい条件を満たさないため、スライドP32にある「catサーバ／クライアント」をGenServer化することから始めます

```elixir:lib/pass_genserver.ex
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というファイルを適当に用意した後、実行します

```elixir
iex> recompile()
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
iex> GenServer.call( pid, { :cat, "a.txt" } )
"I'm a.txt"
```

うまくいきましたね :thumbsup: 

では、存在しないファイルを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が参照できない状態になっています :expressionless:

これは、上記エラー発生時に、サーバがダウンしてしまったことが原因なので、再度サーバ起動してやり直すと．．．

```elixir
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
iex> GenServer.call( pid, { :cat, "a.txt" } )
"I'm a.txt"
```

復活しました

さて通常、こういったケースでは、File.read()を「try ～ catch」で囲むような例外処理を入れて対応したりしますが、これをやり始めると、途端にコードが複雑で見通し悪くなっていきます :sweat:

また、エラー処理に落とし込むべきところを、誤って例外スルーしてしまう、といったバグも作り込みがちです

別のやり方として、プロセスを監視して、ダウンしたら自動的に再起動する、というアプローチもありますが、これもまた面倒です

Elixirには、本質的な処理と、エラー処理を分離するために、「OTPスーパーバイザ」という仕組みがあり、こうした問題をシンプルかつクリーンに解決できます :tada:

# OTPスーパーバイザで例外処理

OTPスーパーバイザを使うための準備として、start_link()を定義し、その中からプロセス起動のための処理として、GenServer.start_link()の呼出を入れます

```elixir:lib/pass_genserver.ex
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です

```elixir
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」に変わった程度です）

```elixir
iex> GenServer.call( :server_process, { :cat, "a.txt" } )
"I'm a.txt"
```

ここまではうまくいきました :kissing:

では、存在しないファイルを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してみると．．．

```elixir
iex> GenServer.call( :server_process, { :cat, "a.txt" } )
"I'm a.txt"
```

今度は大丈夫でした :laughing:

こんな感じで、start_link()の定義だけすれば、プロセスダウンをリカバリーしてくれる「OTPスーパーバイザ」、相当シンプルなのに「やりおるわ」という感じです :heart_eyes:

# あれ？大丈夫なんだっけ？

さて、ここでお察しの良い方は、以下4つの疑問を持ったのでは無いかと思います

- 気軽にプロセスを再起動したけど、負荷とか大丈夫なの？
- 負荷は大丈夫だとしても、プロセスが処理していた状態やデータとかどうなるの？
- 地味に、cdサーバと一緒にpwdサーバもダウンしてるけど、障害切り分けとして、マズくない？
- 「strategy: :one_for_one」というコードの説明が無かったけど、これ一体、何？

こういった疑問に、以降でお答えしていきますね :tropical_drink:

（[次回](http://qiita.com/piacere/items/d06e46b69f47ff349de5)に続く）