本稿は「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)