本稿は「Elixir 1.5 の合理化された child spec」をもとに加筆・補正し、文章を整えました。
スーパーバイザーから起動する子の仕様(child spec)を指定する方法は、Elixir v1.5から改められました。child specの機能とどのように使えばよいのかについてご説明します。
Elixir v1.5以降のchild specの定め方
Elixir v1.5より前は、スーパーバイザーを起動するときのchild specは、つぎのように書いていました。スーパーバイザーはSupervisor.Spec.supervisr/2、ワーカーならSupervisor.Spec.worker/2でそれぞれ子の仕様を定めたわけです。
children = [
Supervisor.Spec.supervisor(MyApp.Repo, []),
Supervisor.Spec.worker(MyApp.MyServer, [:foo]),
]
Supervisor.start_link(children, strategy: :one_for_one)
これでは間違って記述してしまうかもしれません。スーパーバイザーのMyApp.RepoにSupervisor.Spec.worker/2を呼び出したり、ワーカーのはずのMyApp.MyServerをSupervisor.Spec.supervisor/2で起動するというミスが起こりやすいです。
どちらで使うかは、モジュールごとに予め決まっているのが通常でしょう。子を起動するときに考えるものではありません。
子のタイプを指定する:typeオプションだけでなく、:startや:restartについてもおおむね同じです。多くの場合はモジュールを書くときに定めておくべきで、子を起動する段階で決めることではありません。つまり、child specはモジュールの構成に含まれるということです。
Elixir v1.5からこの考え方が採り入れられました。モジュールにchild_spec/1という関数で子の仕様は決めておきます。そのため、子の起動はつぎのように書くだけで済むのです。
children = [
MyApp.Repo,
{MyApp.MyServer, [:foo]},
]
Supervisor.start_link(children, strategy: :one_for_one)
スーパーバイザーが動き出すと、リストに納められた子のすべてに対して、各モジュールのchild_spec/1関数が呼び出されます。この例では、MyApp.Repo.child_spec([])とMyApp.MyServer.child_spec([:foo])からchild specが得られ、それぞれのプロセスが起動するのです。指定したモジュールがchild_spec/1を実装していない場合は、エラーになります。
このようにchild specが合理化されたため、Supervisor.Specは非推奨となりました。子の起動時にchild specをカスタマイズしたいなら、 Supervisor.child_spec/2を使うとよいでしょう。
Supervisor.child_spec({MyApp.MyServer, [:foo]}, shutdown: 10_000)
モジュールがchild_spec/1を実装していない場合には、マップ形式でchild specが定められます。タプル形式より省略ができるので楽になるでしょう。
# MyApp.MyWorker.start_link([:foo]) でプロセスを起動する
%{
id: MyApp.MyWorker, # 必須
start: {MyApp.MyWorker, :start_link, [[:foo]]}, # 必須
restart: :permanent, # 省略可
shutdown: 5_000, # 省略可
type: :worker, # 省略可
modules: [MyApp.MyWorker], # 省略可
}
use GenServerはchild_spec/1を実装する
もっとも、毎回child_spec/1を定義するのは面倒です。そこで、Elixir v1.5からは、use GenServerで自動的にchild_spec/1が実装されます。
defmodule MyServer do
use GenServer
end
IO.inspect MyServer.child_spec([:foo])
# %{
# id: MyServer,
# restart: :permanent,
# shutdown: 5000,
# start: {MyServer, :start_link, [[:foo]]},
# type: :worker,
# }
use GenServerは必ずtype: :workerに定めます。child_spec/1の定義をカスタマイズしたい場合は、use GenServerに引数を追加するだけです。
defmodule MyServer do
use GenServer, restart: :temporary, shutdown: 10_000
end
IO.inspect MyServer.child_spec([:foo])
# %{
# id: MyServer,
# restart: :temporary,
# shutdown: 10_000,
# start: {MyServer, :start_link, [[:foo]]},
# type: :worker,
# }
child_spec/1関数は、GenServerだけでなく、Supervisorを使ったときも自動的に定められます。use Supervisorの場合は必ずtype: :supervisorです。モジュールベースのスーパーバイザーを定義するときには便利でしょう。
defmodule MySupervisor do
use Supervisor
end
IO.inspect MySupervisor.child_spec([:foo])
# %{
# id: MySupervisor,
# restart: :permanent,
# start: {MySupervisor, :start_link, [[:foo]]},
# type: :supervisor,
# }
既存のモジュールもchild_spec/1を実装する
既存のAgentやRegistry、あるいはTaskといったライブラリもchild_spec/1を実装します。つまり、つぎのように書けるということです。わざわざSupervisor.Spec.worker/2を呼ばなくて済みます。
children = [
{Registry, keys: :unique, name: MyApp.Registry},
{Agent, fn -> :ok end},
]
Supervisor.start_link(children, strategy: :one_for_one)
まとめ
Elixir v1.5でchild specが合理化され、モジュール側にchild_spec/1で仕様が定められます。
必要な場合には、モジュールにchild_spec/1を定義しておくのがいいでしょう。use GenServerなどchild_spec/1が自動的に定義されるときは、適切に引数を渡すだけで構いません。
こうして、適切な場所にchild specを定義すれば、指定の間違いが避けられます。有効に活用していきましょう。
Top comments (0)