Elixir
supervisor
OTP
GenServer

はじめてな Elixir(22) 自作のプロセスをスーパバイザの監視下におく

今回は 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 を使わないのを見習いたいところです。しかしながら、後半はやや複雑です。プロセスを再起動した後に、直前の状態をできるだけ維持する機能を設けているためです。

ですので、動作の分かりやすいシンプルさで前半が、最新のスタイルに沿っているという意味では後半のプログラムが欲しくなります。ここに記載するプログラムはそんなところに使えるようにということも意図しています。


準備

mix new --sup Temp とやって雛形を作ります。lib/temp の下に3つのプログラムを置きます。


  • application.ex: プロセスを起動する大元

  • supervisor.ex: スーパバイザ

  • server.ex: サーバ(プロセス)

あとは iex -S mix でコンパイルして実行します。


アプリケーション

アプリケーションは起動時にスーパバイザ経由でプロセスを生成します。ここでは pid を持ち回らずに名前(アトム)でプロセス個体を示すようにして :afo :bar :foo の3つのプロセスがスーパバイザ下で走るように書いてます。


application.ex

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 です。一般名詞のアプリケーションではないことに注意。


スーパバイザ

スーパバイザと言っても関数とコールバックを一つずつ定義するだけです。コールバックでスーパバイザが「何を監視するのか」と「終了したときどう再起動するか」を記述します。


supervisor.ex

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 するように仕組んであります。


server.ex

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.ex

$ 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}



参考文献

Elixir v1.7.4 Supervisor

Elixir v1.7.4 Supervisor.Spec