今回は GenServer を用いて自分で作成したプロセスを OTP のスーパバイザの監視下においてみます。
おさらい
はじめてな Elixir(20) GenServer で定期的なお仕事をする(いい加減つぎに行こうぜ編) で作成したプログラムは以下の特徴を持っています。
- プロセスが定期的にお仕事をする(他のプロセスからの刺激がなくとも)
- GenServer を使うけれども、呼び出す関数は GenServer を明示しない
- pid を明示せずに、プロセスに名前をつけて起動・操作できる、それも複数起動できる
これをスーパバイザ下においてみます。テキスト「プログラミング Elixir」では第17.1節に記述されてます。で、はじめてな Elixir (21) OTPのスーパバイザを使ってみる を書いてて気がついたのですが、この節、ちょっと不思議な書き方になってます。
- 1つの節に2つのスーパバイザ技が記載されている
- 後半は「再起動をまたいだプロセスの状態の管理」以降
- 2つの節に分けても良いぐらいの分量があって、まあまあ両者は独立してる
- 前半と後半でプログラミングスタイルが違う
- 前半は Supervisor.Spec モジュールを用いてる
- これは v1.7.4 では deprecated とされてて使うのを推奨されてない
- 後半は Supervisor.Spec モジュールを用いてない
- 前半は Supervisor.Spec モジュールを用いてる
なのでプログラミングスタイルとしては後半の Supervisor.Spec を使わないのを見習いたいところです。しかしながら、後半はやや複雑です。プロセスを再起動した後に、直前の状態をできるだけ維持する機能を設けているためです。
ですので、動作の分かりやすいシンプルさで前半が、最新のスタイルに沿っているという意味では後半のプログラムが欲しくなります。ここに記載するプログラムはそんなところに使えるようにということも意図しています。
準備
mix new --sup Temp
とやって雛形を作ります。lib/temp の下に3つのプログラムを置きます。
- application.ex: プロセスを起動する大元
- supervisor.ex: スーパバイザ
- server.ex: サーバ(プロセス)
あとは iex -S mix
でコンパイルして実行します。
アプリケーション
アプリケーションは起動時にスーパバイザ経由でプロセスを生成します。ここでは pid を持ち回らずに名前(アトム)でプロセス個体を示すようにして :afo
:bar
:foo
の3つのプロセスがスーパバイザ下で走るように書いてます。
defmodule Temp.Application do
use Application
def start(_type, _args) do
{:ok, _pid} = Temp.Supervisor.start_link(:afo)
{:ok, _pid} = Temp.Supervisor.start_link(:bar)
{:ok, _pid} = Temp.Supervisor.start_link(:foo)
end
end
なお、ここで言うアプリケーションはテキスト18章にもあるように erlang 用語の application です。一般名詞のアプリケーションではないことに注意。
スーパバイザ
スーパバイザと言っても関数とコールバックを一つずつ定義するだけです。コールバックでスーパバイザが「何を監視するのか」と「終了したときどう再起動するか」を記述します。
defmodule Temp.Supervisor do
use Supervisor
def start_link(pname) do
{:ok, _sup} = Supervisor.start_link(__MODULE__, pname)
end
def init(pname) do
child_processes = [worker(Temp.Server, [pname])]
supervise(child_processes, strategy: :one_for_one)
end
end
サーバ(プロセス定義)
プロセスがどんな動作をするのかを記述します。これははじめてな Elixir(20) とほぼ同じです。ただし、スーパバイザを試すために crash 関数を呼ぶと exit するように仕組んであります。
defmodule Temp.Server do
@behaviour GenServer
def start_link(pname) do
GenServer.start_link(__MODULE__, nil, name: pname)
end
def last(pname) do
GenServer.call(pname, :last)
end
def list(pname) do
GenServer.call(pname, :list)
end
def crash(pname) do
GenServer.call(pname, :crash)
end
@impl GenServer
def init(_void) do
set_interval()
{:ok, []}
end
@interval 2000
defp set_interval() do
Process.send_after(self(), :wakeup, @interval)
end
@impl GenServer
def handle_info(:wakeup, temp_list) do
new_temp_list = append_new(temp_list)
set_interval()
{:noreply, new_temp_list}
end
defp append_new([]) do
[0]
end
defp append_new([head | tail]) do
[head + 1 | [head | tail]]
end
@impl GenServer
def handle_cast(:next, []) do
{:noreply, [0]}
end
@impl GenServer
def handle_cast(:next, [head | tail]) do
{:noreply, [head + 1 | [head | tail]]}
end
@impl GenServer
def handle_call(:last, _from, []) do
{:reply, {:error, []}, []}
end
@impl GenServer
def handle_call(:last, _from, [head | tail]) do
{:reply, {:ok, head}, [head | tail]}
end
@impl GenServer
def handle_call(:list, _from, list) do
{:reply, {:ok, list}, list}
end
@impl GenServer
def handle_call(:crash, _from, _list) do
exit(:crash)
end
end
start_link の行は以下にしておくと、いろいろとデバッグ情報が取れるようになります。詳しくはテキストの 16.2 節を。
GenServer.start_link(__MODULE__, nil, [name: pname, debug: [:trace, :statistics]])
実行
元のプログラムの特徴を維持したまま、さらにスーパバイザによる監視がされていることを確認します。
$ iex -S mix
Erlang/OTP 21 [erts-10.1.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Temp.Server.last(:afo) # プロセス :afo が起動されてます
{:error, []}
iex(2)> Temp.Server.last(:afo) # :error で、まだ値がないこと知らせます
{:error, []}
iex(3)> Temp.Server.last(:afo) # 2秒ごとに新しい値をつけます
{:ok, 0}
iex(4)> Temp.Server.last(:afo)
{:ok, 3}
iex(5)> Temp.Server.list(:afo) # 10秒ぐらいするとこれぐらいのリストに
{:ok, [4, 3, 2, 1, 0]}
iex(6)> Temp.Server.last(:bar) # プロセス :bar の稼働を確認します
{:ok, 7}
iex(7)> Temp.Server.last(:foo) # プロセス :foo の稼働も確認します
{:ok, 8}
iex(8)> Temp.Server.crash(:afo) # プロセス :afo を強制終了させます
01:07:38.549 [error] GenServer :afo terminating
** (stop) :crash
(temp) lib/temp/server.ex:78: Temp.Server.handle_call/3
(stdlib) gen_server.erl:661: :gen_server.try_handle_call/4
(stdlib) gen_server.erl:690: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.137.0>): :crash
State: [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Client #PID<0.137.0> is alive
(stdlib) gen.erl:169: :gen.do_call/4
(elixir) lib/gen_server.ex:921: GenServer.call/3
(stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
(elixir) src/elixir.erl:265: :elixir.eval_forms/4
(iex) lib/iex/evaluator.ex:249: IEx.Evaluator.handle_eval/5
(iex) lib/iex/evaluator.ex:229: IEx.Evaluator.do_eval/3
(iex) lib/iex/evaluator.ex:207: IEx.Evaluator.eval/3
(iex) lib/iex/evaluator.ex:94: IEx.Evaluator.loop/1
** (exit) exited in: GenServer.call(:afo, :crash, 5000)
** (EXIT) :crash
(elixir) lib/gen_server.ex:924: GenServer.call/3
iex(8)> Temp.Server.last(:afo) # プロセス :afo がどうなるかと言うと
{:error, []}
iex(9)> Temp.Server.last(:afo) # ちゃんと再起動されてます
{:ok, 0}
iex(10)> Temp.Server.list(:afo) # exit 前には戻りませんが動いてはいます
{:ok, [2, 1, 0]}
iex(11)> Temp.Server.last(:bar) # :afo に関係なく :bar も動いてます
{:ok, 21}
iex(12)> Temp.Server.last(:foo) # :foo も唯我独尊で動いてます
{:ok, 22}