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

# 環境

```shell-session: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本との対応関係。

- マネージャー：`Band` は `Supervisor`（`band_supervisor.erl`）
- ミュージシャン：`Band.Musician` は `GenServer`（`musicians.erl`）

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

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

```shell-session:sh
$ mix new band
$ cd band
$ mkdir lib/band
$ touch lib/band/musician.ex
```
```elixir: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
```
```shell-session:sh
$ mix format
$ iex -S mix
```
```elixir: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/1` で `terminate(:normal, state)` ならば通常終了。
それ以外の `terminate/2` が呼ばれるとエラーが出ることがわかる。

## バンドのスーパバイザ

[Supervisor — Elixir vx.x.x](https://hexdocs.pm/elixir/Supervisor.html#content)

```elixir: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
      # おおらか：一人ずつ解雇する（制限時間内に３回のミスまでは許す）
      :lenient -> _init(:one_for_one, 3)
      # 怒りっぽい：ミスった奴と後続らは解雇（２回）
      :angry -> _init(:rest_for_one, 2)
      # 間抜け：一人でもミスったら全員解雇（１回）
      :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
```

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

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

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

```elixir: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) はミスった・・・ # １回目
ヘタクソ！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) はミスった・・・ # ２回目
ヘタクソ！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) はミスった・・・ # ３回目
ヘタクソ！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) はミスった・・・ # ４回目
ヘタクソ！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
```

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

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

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

```elixir: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) はミスった・・・ # １回目
ヘタクソ！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) はミスった・・・ # ２回目
ヘタクソ！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) はミスった・・・ # ３回目
ヘタクソ！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
```

### ※ 間抜けなマネージャー

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

```elixir: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) はミスった・・・ # １回目
ヘタクソ！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) はミスった・・・ # ２回目
ヘタクソ！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` の関数をテストするために下記を追加。

```elixir: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
```
```elixir: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 ではこの戦略が非推奨になっているので次回に回すことにする。