■ ElixirのConcurrent Programmingについてまとめた過去記事
Elixir Concurrent Programming(1) - Spawn - Qiita
Elixir Concurrent Programming(2) - GenServer - Qiita
Elixir Concurrent Programming(3) - Supervisors - Qiita
Elixir Concurrent Programming(4) - Task and Agent -Qiita
OTP supervisor behaviorは、worker processを監視し、停止したプロセスを再起動する機能を提供してくれます。
以下、公式サイトSupervisor behaviourで挙げられているコードを参考としつつ、Supervisorの機能を確認していきたいと思います。
#1.プロジェクト作成
stackプロジェクトを作成します。
mix new --sup stack
--supが付いているので、application.exというファイルが生成されます。このファイルについては後で説明します。
# mix new --sup stack
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/stack.ex
* creating lib/stack/application.ex ### supオプションでこれが作成される
* creating test
* creating test/test_helper.exs
* creating test/stack_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd stack
mix test
Run "mix help" for more commands.
このプロジェクトで動作するプロセスは以下の3個になります。SupervisorがBackupとServerを起動し、監視し、停止時に再起動します。このプロセスの関係はapplication.exで定義します。
Supervisor ------ Backup
|
|-- Server
#2.Stack.Server
Stack.Serverは、Stackサービスを提供するGenServerです。initでStack.Backup.get()で初期化用のstateを取得します。terminate時にカレントのstateをStack.Backup.updateでバックアップします。強制的にプロセスを停止させるためにhandle_cast(:kill, stack)を実装しています。
defmodule Stack.Server do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def pop do
GenServer.call __MODULE__, :pop
end
def push(head) do
GenServer.cast __MODULE__, {:push, head}
end
## Callbacks
def init(_) do
{:ok, Stack.Backup.get() } ### 初期化時にBackupサーバを参照
end
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
def handle_cast({:push, head}, tail) do
{:noreply, [head | tail]}
end
def handle_cast(:kill, stack) do ### 強制的にプロセスを停止させる
a = 1 / 0
{:noreply, [stack]}
end
def terminate(_reason, current_stack) do
Stack.Backup.update(current_stack) ### 停止時にBackupサーバを更新
end
end
#3.Stack.Backup
Stack.Backup GenServerは、Stack.Serverがstackをバックアップするためのサーバです。Stack.Serverは初期値をStack.Backupから取得します。またStack.Serverは、停止時にStack.Backupにバックアップを取り、再起動時にそのバックアップを取得することで、停止時のstateのを復元します。
defmodule Stack.Backup do
use GenServer
@me __MODULE__
def start_link(initial_stack) do
GenServer.start_link(__MODULE__, initial_stack, name: @me)
end
def get() do
GenServer.call(@me, { :get })
end
def update(new_stack) do
GenServer.cast(@me, { :update, new_stack })
end
# Server implementation
def init(initial_stack) do
{ :ok, initial_stack }
end
def handle_call({ :get }, _from, current_stack ) do
{ :reply, current_stack, current_stack }
end
def handle_cast({ :update, new_stack }, _current_stack) do
{ :noreply, new_stack }
end
end
#4.application.ex
childrenに起動するプロセスをリストします。Stack.Backupには初期値を指定します。Stack.ServerはStack.Backupから初期値を得るので、初期値は指定しません。
:rest_for_one戦略は次のようなものです。childrenリストのサーバが死んだときに、自分より後にリストされているサーバも停止する。その後で停止サーバを全て再起動する。その他の戦略は次の通りです。:one_for_one は単に死んだサーバを再起動するのみで、デフォルトです。:one_for_all はサーバが死んだら、他の全てのサーバも停止して、全て再起動する戦略です。
defmodule Stack.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
def start(_type, initial_stack) do
# List all child processes to be supervised
children = [
{ Stack.Backup, initial_stack},
{ Stack.Server, nil},
]
opts = [strategy: :rest_for_one, name: Stack.Supervisor]
Supervisor.start_link(children, opts)
end
end
OTP Applicationの実装です。mix.exsにて本ApplicationのトップのmoduleがStackであることを教え、初期値を与えます。registeredオプションでStack.ServerやStack.Backupの名前のユニーク性(Node単位)を確保します。
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {Stack.Application, [[:hello]]},
registered: [
Stack.Server,
Stack.Backup,
]
]
end
#5.Supervisorの動作の確認
以下、Stack.Serverがエラーを起こし停止します。自動的に再起動するのですが、停止した時のstateを保持しているのがわかります。後ろでStack.Backupが機能しているおかげです。
# iex -S mix
Generated stack app
iex(1)> Stack.Server.pop
[:hello]
iex(2)> Stack.Server.push(:world)
:ok
iex(3)> Stack.Server.push(:world2)
:ok
iex(4)> Stack.Server.push(:world3)
:ok
iex(5)> GenServer.cast Stack.Server, :kill
:ok
iex(6)>
12:56:07.504 [error] GenServer Stack.Server terminating
** (ArithmeticError) bad argument in arithmetic expression
(stack) lib/stack/server.ex:32: Stack.Server.handle_cast/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:"$gen_cast", :kill}
State: [:world3, :world2, :world]
nil
iex(7)> Stack.Server.pop
:world3
iex(8)> Stack.Server.pop
:world2
以上です
■ OTP supervisor behavior関連の過去記事
東京電力電力供給状況監視 - Phoenix Channel - Qiita
Phoenix Channelで作る最先端Webアプリ - Elixier Application編 - Qiita
Phoenix Channelで作る最先端Webアプリ - DynamicSupervisor編 - Qiita