この記事は Elixir その2 Advent Calendar 2020 19日目です。
前日は、「[Elixir] GenServer.init関数で重い処理」でした。
はじめに
Elixirの「Let it crash」はAWESOMEです。プロセスが再起動されるのを見るたびに感動します。そのプロセス管理に重要になってくるのがSupervisor
ですが、child_spec
がちょっとややこしくないですか?僕はそう思ったので、今後迷わないように構文として整理しました。サンプルコードは手作りです。
僕は「Elixir in Action」by Sasa JuricでElixirの基礎を学びました。色んなアイデアはそこにあります。
child_spec
ドキュメントによると、child_spec
は僕たちが書いたモジュールをどのように扱ってほしいのか(起動、停止、再起動など)をSupervisor
に教えるためのものだそうです。ですので、プロセスをSupervisor
に管理させたい場合は必ずchild_spec
が必要となることになります。
データ構造
色んな定義のやり方
HelloWorker
というモジュールをSupervisor
に管理させたい場合のサンプルです。
基本形1(Supervisor.start_linkに直書き)
引数の渡し方が若干ややこしく感じました。渡せる引数はひとつだけ。複数渡したい場合は、タプルやリスト等を使う。Supervisor
に子プロセスを追加するときにエラーが出たとき、僕の場合、引数の渡し方が間違っている場合がほどんどです。
defmodule HelloWorker do
# 重要: ここでは意図的に何も`use`していない。
def start_link(word: word) do
GenServer.start_link(__MODULE__, word)
end
def init(word) do
IO.puts("Started: #{word}")
{:ok, word}
end
end
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
[
%{
id: HelloWorker,
start: {HelloWorker, :start_link, [[word: "Elixirたのしいですね〜"]]}
}
],
strategy: :one_for_one
)
Started: Elixirたのしいですね〜
{:ok, #PID<0.528.0>}
# Supervisorが子プロセスを持っていることを確認。
iex> Supervisor.count_children sup
%{active: 1, specs: 1, supervisors: 0, workers: 1}
iex> Supervisor.which_children sup
[{HelloWorker, #PID<0.529.0>, :worker, [HelloWorker]}]
# 子プロセスを停止してみる。
iex> GenServer.stop(pid(0, 529, 0))
Started: Elixirたのしいですね〜
:ok
# PIDが新しくなっていることを確認。
iex> Supervisor.which_children sup
[{HelloWorker, #PID<0.533.0>, :worker, [HelloWorker]}]
基本形2(子モジュールにchild_spec関数を定義)
ドキュメントによると、こちらの構文が推奨されてます。子モジュールに変更があったときにchild_spec
を書き換えることを忘れるのを防ぐのが理由です。
defmodule HelloWorker do
# 重要: ここでは意図的に何も`use`していない。
def child_spec(args) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [args]},
}
end
def start_link(word: word) do
GenServer.start_link(__MODULE__, word)
end
def init(word) do
IO.puts("Started: #{word}")
{:ok, word}
end
end
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
[{HelloWorker, [word: "Elixirたのしいですね〜"]}],
strategy: :one_for_one
)
Started: Elixirたのしいですね〜
{:ok, #PID<0.183.0>}
省略形1
use GenServer
やuse Task
をモジュールに含めると、それらがデフォルトのchild_spec
関数を挿入してくれるので、その関数の定義に従えば自分でchild_spec
関数を定義するは必要なし。それが気に入らない場合は、自分で定義する。
GenServer - How to supervise
Task - Supervised tasks
defmodule HelloWorker do
use GenServer
def start_link(word: word) do
GenServer.start_link(__MODULE__, word)
end
def init(word) do
IO.puts("Started: #{word}")
{:ok, word}
end
end
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
[
# HelloWorker.child_spec(word: "Elixirたのしいですね〜")が呼ばれる。
{HelloWorker, [word: "Elixirたのしいですね〜"]}
],
strategy: :one_for_one
)
Started: Elixirたのしいですね〜
{:ok, #PID<0.126.0>}
省略形2
子モジュールが引数を無視する場合にのみ使えるパターン。子モジュール名を渡すと、Supervisor
はHelloWorker
を{HelloWorker, []}
であると解釈してくれる。
defmodule HelloWorker do
use GenServer
def start_link(_args) do
GenServer.start_link(__MODULE__, nil)
end
def init(_args) do
word = "Elixirたのしいですね〜"
IO.puts("Started: #{word}")
{:ok, word}
end
end
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
[
# HelloWorker.child_spec([])が呼ばれる。
HelloWorker
],
strategy: :one_for_one
)
Started: Elixirたのしいですね〜
{:ok, #PID<0.148.0>}
同じモジュールから複数のプロセスを起動したい場合
-
use GenServer
等が挿入してくれるデフォルトのchild_spec/1
は__MODULE__
をIDとしているので、複数のプロセスを起動する場合は使用できない。 -
use GenServer
等を使用するとしても、プロセスごとに一意のIDを生成するように自分でchild_spec/1
を定義すること。
defmodule HelloWorker do
use GenServer
def child_spec([word: word] = args) do
%{
id: {__MODULE__, word},
start: {__MODULE__, :start_link, [args]},
}
end
def start_link(word: word) do
GenServer.start_link(__MODULE__, word)
end
def init(word) do
IO.puts("Started: #{word}")
{:ok, word}
end
end
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
[
# HelloWorker.child_spec(word: "Elixirたのしいですね〜")が呼ばれる。
{HelloWorker, [word: "Elixirたのしいですね〜"]},
# HelloWorker.child_spec(word: "ナウでヤングでcoolなNervesフレームワーク")が呼ばれる。
{HelloWorker, [word: "ナウでヤングでcoolなNervesフレームワーク"]}
],
strategy: :one_for_one
)
Started: Elixirたのしいですね〜
Started: ナウでヤングでcoolなNervesフレームワーク
{:ok, #PID<0.156.0>}
さいごに
因みにElixirとナウでヤングでcoolなNervesを使っての今どきの電子工作は本当に楽しいので、オススメします。
今後更に新しいことを学んだら随時内容も更新していこうと思います。
明日は「[Elixir] Referenceの作り方」です。引き続き、Elixirを楽しみましょう。