LoginSignup
5
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-11-19

今回は 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

5
1
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
5
1