LoginSignup
13
11

More than 5 years have passed since last update.

Elixir: Supervisorを使ってプロセス管理を行う

Last updated at Posted at 2016-04-27

この記事は先日の記事の続きとなります.

プロセスでエラーが発生したら, 何が起こるのか?

試しに実際にエラーを起こしてみましょう

これまで作成してきたモジュールに, 次のような関数を追加して..

  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を作ってみましょう.

remote_agent/supervisor.ex
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
remote_agent3.ex
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を監視対象とします.

remote_agent/supervisor.ex
  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_callhandle_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

コード全体はこんな感じになります.

remote_agent3.ex
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
remote_agent/supervisor.ex
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

13
11
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
13
11