環境
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
)
登場するミュージシャンは、ボーカル、ベース、キーター、ドラムの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/1
で terminate(:normal, state)
ならば通常終了。
それ以外の terminate/2
が呼ばれるとエラーが出ることがわかる。
バンドのスーパバイザ
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 ではこの戦略が非推奨になっているので次回に回すことにする。