Help us understand the problem. What is going on with this article?

すごいE本 第17章 前半戦 on Elixir (Supervisor)

環境

sh
$ lsb_release -d
Description:    Ubuntu 18.04.2 LTS

$ elixir -v
Erlang/OTP 21 [erts-10.3.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.8.1 (compiled with Erlang/OTP 20)

17.3 バンドで練習

E本との対応関係。

  • マネージャー:BandSupervisorband_supervisor.erl
  • ミュージシャン:Band.MusicianGenServermusicians.erl

登場するミュージシャンは、ボーカル、ベース、キーター、ドラムの4種(各1名)。
バンドメンバーを募集すると必ずドラマーだけがヘタクソだという設定。
マネージャーの性格は、おおらか、怒りっぽい、間抜けの3種。
各マネージャーは解雇する基準がそれぞれ異なるという設定。
ドラマーが間違える度に、マネージャーは何人か解雇し、再募集をかける。

まずはミュージシャンを GenServer として実装する。

sh
$ mix new band
$ cd band
$ mkdir lib/band
$ touch lib/band/musician.ex
band/lib/band/musician.ex
defmodule Band.Musician do
  # Elixir の場合 use でビヘイビア規定の子仕様が設定される
  # 既定値を更新する例:use GenServer, restart: :temporary, shutdown: 3_000
  # スーパーバイザの子(ワーカー)になるとき上書きされるのが前提
  use GenServer
  alias __MODULE__, as: Me

  defstruct [:name, :role, :skill]

  defp new(role, skill) when is_atom(role) and is_atom(skill) do
    %Me{
      name: pick_name(),
      role: Atom.to_string(role),
      skill: skill
    }
  end

  @firstnames [
    "Valerie",
    "Arnold",
    "Carlos",
    "Dorothy",
    "Keesha",
    "Phoebe",
    "Ralphie",
    "Tim",
    "Wanda",
    "Janet"
  ]

  @lastnames [
    "Frizzle",
    "Perlstein",
    "Ramon",
    "Ann",
    "Franklin",
    "Terese",
    "Tennelli",
    "Jamal",
    "Li",
    "Perlstein"
  ]

  defp pick_name do
    # random モジュールは非推奨(rand ならば seed/1 不要)
    first = Enum.at(@firstnames, :rand.uniform(10) - 1)
    last = Enum.at(@lastnames, :rand.uniform(10) - 1)
    "#{first} #{last}"
  end

  # ミュージシャン構造体の表示用文字列
  defp string(me = %Me{}), do: "#{me.name}(#{me.skill} #{me.role})"

  def start_link(role, skill) do
    GenServer.start_link(Me, [role, skill], name: role)
  end

  def stop(role), do: GenServer.call(role, :stop)

  @max_delay 3_000
  @delay 750

  @impl true
  def init([role, skill]) do
    # マネージャーがミュージシャンを解雇するのを捕捉するため
    Process.flag(:trap_exit, true)
    me = new(role, skill)
    IO.puts("#{string(me)} は部屋に入った。")

    # 初回の演奏結果が出るまでの時間にバラツキを持たせている
    {:ok, me, :rand.uniform(@max_delay)}
  end

  @impl true
  def handle_call(:stop, _from, me = %Me{}) do
    {:stop, :normal, :ok, me}
  end

  @impl true
  # good skill な奴は決して演奏を失敗しない
  def handle_info(:timeout, me = %Me{skill: :good}) do
    IO.puts("#{string(me)} の音はいいね!")
    {:noreply, me, @delay}
  end

  @impl true
  # bad skill な奴は一定の確率で演奏を失敗する
  def handle_info(:timeout, me = %Me{skill: :bad}) do
    case :rand.uniform(5) do
      1 ->
        IO.puts("#{string(me)} はミスった・・・")
        {:stop, :bad_note, me}

      _ ->
        IO.puts("#{string(me)} の音はいいね!")
        {:noreply, me, @delay}
    end
  end

  @impl true
  def terminate(:normal, me = %Me{}) do
    IO.puts("#{string(me)} は部屋を出た。")
  end

  @impl true
  def terminate(:bad_note, me = %Me{}) do
    IO.puts("ヘタクソ!#{string(me)} をバンドから蹴り出した!")
  end

  @impl true
  # ワーカーの場合、スーパーバイザからのシグナルをここで捕捉する
  def terminate(:shutdown, me = %Me{}) do
    IO.puts("""
    マネージャーは怒りバンドを解散させた!
    #{string(me)} は地下鉄へ去っていった。
    """)
  end

  @impl true
  def terminate(_reason, me = %Me{}) do
    IO.puts("#{string(me)} は追い出された。")
  end
end
sh
$ mix format
$ iex -S mix
iex
iex(1)> Band.Musician.start_link(:bass, :bad)
Valerie Franklin(bad bass) は部屋に入った。
{:ok, #PID<0.155.0>}
Valerie Franklin(bad bass) の音はいいね!
Valerie Franklin(bad bass) の音はいいね!
Valerie Franklin(bad bass) の音はいいね!
Valerie Franklin(bad bass) はミスった・・・
ヘタクソ!Valerie Franklin(bad bass) をバンドから蹴り出した!
iex(2)> 
17:35:02.634 [error] GenServer :bass terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Valerie Franklin", role: "bass", skill: :bad}
** (EXIT from #PID<0.153.0>) shell process exited with reason: :bad_note

iex(1)> Band.Musician.start_link(:bass, :good)
Keesha Ramon(good bass) は部屋に入った。
{:ok, #PID<0.139.0>}
Keesha Ramon(good bass) の音はいいね!
Keesha Ramon(good bass) の音はいいね!
Keesha Ramon(good bass) の音はいいね!
iex(2)> Band.Musician.stop(:bass)
Keesha Ramon(good bass) は部屋を出た。
:ok

stop/1terminate(:normal, state) ならば通常終了。
それ以外の terminate/2 が呼ばれるとエラーが出ることがわかる。

バンドのスーパバイザ

Supervisor — Elixir vx.x.x

band/lib/band.ex
defmodule Band do
  use Supervisor
  alias __MODULE__, as: Me
  alias __MODULE__.Musician

  def start_link(type) do
    Supervisor.start_link(Me, type, name: Me)
  end

  @max_seconds 60
  @shutdown 1_000

  @impl true
  def init(type) do
    case type do
      # おおらか:一人ずつ解雇する(制限時間内に3回のミスまでは許す)
      :lenient -> _init(:one_for_one, 3)
      # 怒りっぽい:ミスった奴と後続らは解雇(2回)
      :angry -> _init(:rest_for_one, 2)
      # 間抜け:一人でもミスったら全員解雇(1回)
      :jerk -> _init(:one_for_all, 1)
    end
  end

  defp _init(strategy, max_restarts) do
    opts = [
      strategy: strategy,
      max_restarts: max_restarts, # 既定値:3
      max_seconds: @max_seconds   # 既定値:5
    ]

    children = [
      # ボーカルはいつだって必要
      child_spec(:singer, :good, :permanent),
      # ベースは一回こっきり
      child_spec(:bass, :good, :temporary),
      # ドラム、キーター:解雇した場合に限って再度募集
      child_spec(:drum, :bad, :transient),
      child_spec(:keytar, :good, :transient)
    ]

    Supervisor.init(children, opts)
  end

  # Child specification
  defp child_spec(role, skill, restart) do
    %{
      id: role,
      start: {Musician, :start_link, [role, skill]},
      restart: restart,   # 既定値::permanent
      shutdown: @shutdown # 既定値:5_000
      # type: :worker,
      # modules: [Musician]
    }
  end
end

これからマネージャーを起動するが、演奏が1分を超えることはまずなく、
ドラマーの連続ミスでマネージャーがバンドを見限る。
言い換えるならば、ほぼ確実に制限時間内で再起動回数制限に引っかかる。

※ おおらかなマネージャー

おおらかなマネージャーは1分間で3回まではミスを許す。

iex
iex(1)> Band.start_link(:lenient)
Janet Ramon(good singer) は部屋に入った。
Arnold Perlstein(good bass) は部屋に入った。
Tim Perlstein(bad drum) は部屋に入った。
Ralphie Frizzle(good keytar) は部屋に入った。
{:ok, #PID<0.149.0>}
Tim Perlstein(bad drum) の音はいいね!
Ralphie Frizzle(good keytar) の音はいいね!
Tim Perlstein(bad drum) はミスった・・・ # 1回目
ヘタクソ!Tim Perlstein(bad drum) をバンドから蹴り出した!
iex(2)> 
18:06:42.802 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Tim Perlstein", role: "drum", skill: :bad}

Tim Franklin(bad drum) は部屋に入った。
Ralphie Frizzle(good keytar) の音はいいね!
Janet Ramon(good singer) の音はいいね!
Ralphie Frizzle(good keytar) の音はいいね!
Arnold Perlstein(good bass) の音はいいね!
Tim Franklin(bad drum) はミスった・・・ # 2回目
ヘタクソ!Tim Franklin(bad drum) をバンドから蹴り出した!
iex(2)> 
18:06:46.205 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Tim Franklin", role: "drum", skill: :bad}

Janet Perlstein(bad drum) は部屋に入った。
Janet Ramon(good singer) の音はいいね!
Ralphie Frizzle(good keytar) の音はいいね!
Arnold Perlstein(good bass) の音はいいね!
Janet Perlstein(bad drum) はミスった・・・ # 3回目
ヘタクソ!Janet Perlstein(bad drum) をバンドから蹴り出した!
iex(2)> 
18:06:49.364 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Janet Perlstein", role: "drum", skill: :bad}

Ralphie Terese(bad drum) は部屋に入った。
Janet Ramon(good singer) の音はいいね!
Ralphie Frizzle(good keytar) の音はいいね!
Arnold Perlstein(good bass) の音はいいね!
Ralphie Terese(bad drum) はミスった・・・ # 4回目
ヘタクソ!Ralphie Terese(bad drum) をバンドから蹴り出した!
iex(2)> 
18:06:51.862 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Ralphie Terese", role: "drum", skill: :bad}

マネージャーは怒りバンドを解散させた!
Ralphie Frizzle(good keytar) は地下鉄へ去っていった。

マネージャーは怒りバンドを解散させた!
Arnold Perlstein(good bass) は地下鉄へ去っていった。

マネージャーは怒りバンドを解散させた!
Janet Ramon(good singer) は地下鉄へ去っていった。

** (EXIT from #PID<0.147.0>) shell process exited with reason: shutdown

ドラマーの4回めの失敗でマネージャーは業を煮やしバンドを見限った。

※ 怒りっぽいマネージャー

怒りっぽいマネージャーはドラマーがミスると、ついでにキーターも解雇する。
ミスは1分間で2回までは許す。

iex
iex(1)> Band.start_link(:angry)
Keesha Ann(good singer) は部屋に入った。
Janet Perlstein(good bass) は部屋に入った。
Keesha Ramon(bad drum) は部屋に入った。
Janet Perlstein(good keytar) は部屋に入った。
Janet Perlstein(good bass) の音はいいね!
{:ok, #PID<0.135.0>}
Keesha Ramon(bad drum) はミスった・・・ # 1回目
ヘタクソ!Keesha Ramon(bad drum) をバンドから蹴り出した!
iex(2)> 
18:37:54.451 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Keesha Ramon", role: "drum", skill: :bad}

# キーターは巻き添えを食らう
マネージャーは怒りバンドを解散させた! # ボーカルとベースは残ってるけどね
Janet Perlstein(good keytar) は地下鉄へ去っていった。

Dorothy Terese(bad drum) は部屋に入った。
Janet Ann(good keytar) は部屋に入った。
Dorothy Terese(bad drum) はミスった・・・ # 2回目
ヘタクソ!Dorothy Terese(bad drum) をバンドから蹴り出した!
iex(2)> 
18:37:59.113 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Dorothy Terese", role: "drum", skill: :bad}

マネージャーは怒りバンドを解散させた!
Janet Ann(good keytar) は地下鉄へ去っていった。

Phoebe Jamal(bad drum) は部屋に入った。
Keesha Tennelli(good keytar) は部屋に入った。
Phoebe Jamal(bad drum) はミスった・・・ # 3回目
ヘタクソ!Phoebe Jamal(bad drum) をバンドから蹴り出した!
iex(2)> 
18:38:02.144 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Phoebe Jamal", role: "drum", skill: :bad}

マネージャーは怒りバンドを解散させた!
Tim Perlstein(good keytar) は地下鉄へ去っていった。

マネージャーは怒りバンドを解散させた!
Janet Perlstein(good bass) は地下鉄へ去っていった。

マネージャーは怒りバンドを解散させた!
Keesha Ann(good singer) は地下鉄へ去っていった。

** (EXIT from #PID<0.133.0>) shell process exited with reason: shutdown

※ 間抜けなマネージャー

間抜けなマネージャーは1回しかミスを許さないし、ミスが出る度に全員解雇する。

iex
iex(1)> Band.start_link(:jerk)
Phoebe Li(good singer) は部屋に入った。
Phoebe Li(good bass) は部屋に入った。
Tim Li(bad drum) は部屋に入った。
Valerie Frizzle(good keytar) は部屋に入った。
{:ok, #PID<0.144.0>}
Tim Li(bad drum) はミスった・・・ # 1回目
ヘタクソ!Tim Li(bad drum) をバンドから蹴り出した!
Phoebe Li(good bass) の音はいいね!
Valerie Frizzle(good keytar) の音はいいね!
iex(2)> 
19:00:53.098 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Tim Li", role: "drum", skill: :bad}

マネージャーは怒りバンドを解散させた!
Valerie Frizzle(good keytar) は地下鉄へ去っていった。

マネージャーは怒りバンドを解散させた!
Phoebe Li(good bass) は地下鉄へ去っていった。

マネージャーは怒りバンドを解散させた!
Phoebe Li(good singer) は地下鉄へ去っていった。

# もうベースは募集しない
Phoebe Perlstein(good singer) は部屋に入った。
Arnold Ann(bad drum) は部屋に入った。
Janet Li(good keytar) は部屋に入った。
Arnold Ann(bad drum) はミスった・・・ # 2回目
ヘタクソ!Arnold Ann(bad drum) をバンドから蹴り出した!
iex(2)> 
19:01:02.020 [error] GenServer :drum terminating
** (stop) :bad_note
Last message: :timeout
State: %Band.Musician{name: "Arnold Ann", role: "drum", skill: :bad}

マネージャーは怒りバンドを解散させた!
Janet Li(good keytar) は地下鉄へ去っていった。

マネージャーは怒りバンドを解散させた!
Phoebe Perlstein(good singer) は地下鉄へ去っていった。

** (EXIT from #PID<0.142.0>) shell process exited with reason: shutdown

17.4 動的な監視

標準のスーパバイザを動的に使う

Supervisor の関数をテストするために下記を追加。

band/lib/band.ex
defmodule Band do
  ...
  # 追加
  @impl true
  def init(:test) do
    # こいつらは決してミスをしない!
    [
      child_spec(:singer, :good, :permanent),
      child_spec(:bass, :good, :temporary),
      child_spec(:drum, :good, :transient),
      child_spec(:keytar, :good, :transient)
    ]
    |> Supervisor.init(strategy: :one_for_one)
  end
  ...
end
iex
iex(1)> Band.start_link(:test)
Arnold Ann(good singer) は部屋に入った。
Janet Perlstein(good bass) は部屋に入った。
Valerie Ramon(good drum) は部屋に入った。
Phoebe Ramon(good keytar) は部屋に入った。
{:ok, #PID<0.135.0>}
Phoebe Ramon(good keytar) の音はいいね!
Janet Perlstein(good bass) の音はいいね!
Valerie Ramon(good drum) の音はいいね!
Arnold Ann(good singer) の音はいいね!
...
iex(2)> Supervisor.which_children(Band)
[
  {:keytar, #PID<0.148.0>, :worker, [Band.Musician]},
  {:drum, #PID<0.147.0>, :worker, [Band.Musician]},
  {:bass, #PID<0.146.0>, :worker, [Band.Musician]},
  {:singer, #PID<0.145.0>, :worker, [Band.Musician]}
]
...
iex(3)> Supervisor.terminate_child(Band, :drum)
マネージャーは怒りバンドを解散させた!
Valerie Ramon(good drum) は地下鉄へ去っていった。

:ok
...
iex(4)> Supervisor.terminate_child(Band, :singer)
マネージャーは怒りバンドを解散させた!
Arnold Ann(good singer) は地下鉄へ去っていった。

:ok
...
iex(5)> Supervisor.restart_child(Band, :singer)
Keesha Ramon(good singer) は部屋に入った。
{:ok, #PID<0.143.0>}
...
iex(6)> Supervisor.count_children(Band)
%{active: 3, specs: 4, supervisors: 0, workers: 4}
...
iex(7)> Supervisor.delete_child(Band, :drum)
:ok
...
iex(8)> Supervisor.restart_child(Band, :drum)
{:error, :not_found}
...
iex(9)> Supervisor.count_children(Band)
%{active: 3, specs: 3, supervisors: 0, workers: 3}

次回

この後、:simple_one_for_one のシンプルで無い話に続くのだが、
Elixir ではこの戦略が非推奨になっているので次回に回すことにする。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした