LoginSignup
12
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-08-17

前回に続き、Elixirで遠隔PCに侵入風 のパラレルワールド的展開をいきます :relaxed:

image.png

今回の内容、上記スライドによる社内勉強会をやっていたときから、スライドに入れるべきか、モヤモヤしてた内容なんだけど、tokyo.exのおーはらさんの記事の勢いに元気付けられて、展開した内容なんですよ :grin:

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

前回 作ったpwdサーバは、今回お話したい条件を満たさないため、スライドP32にある「catサーバ/クライアント」をGenServer化することから始めます

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というファイルを適当に用意した後、実行します

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:

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

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()の呼出を入れます

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です

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"

ここまではうまくいきました :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してみると...

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:

次回に続く)

12
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
3