DEV Community

gumi TECH for gumi TECH Blog

Posted on

Elixir: child specの機能

本稿は「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)
Enter fullscreen mode Exit fullscreen mode

これでは間違って記述してしまうかもしれません。スーパーバイザーのMyApp.RepoSupervisor.Spec.worker/2を呼び出したり、ワーカーのはずのMyApp.MyServerSupervisor.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)
Enter fullscreen mode Exit fullscreen mode

スーパーバイザーが動き出すと、リストに納められた子のすべてに対して、各モジュールの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)
Enter fullscreen mode Exit fullscreen mode

モジュールが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], # 省略可
}
Enter fullscreen mode Exit fullscreen mode

use GenServerchild_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,
# }
Enter fullscreen mode Exit fullscreen mode

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,
# }
Enter fullscreen mode Exit fullscreen mode

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,
# }
Enter fullscreen mode Exit fullscreen mode

既存のモジュールもchild_spec/1を実装する

既存のAgentRegistry、あるいは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)
Enter fullscreen mode Exit fullscreen mode

まとめ

Elixir v1.5でchild specが合理化され、モジュール側にchild_spec/1で仕様が定められます。

必要な場合には、モジュールにchild_spec/1を定義しておくのがいいでしょう。use GenServerなどchild_spec/1が自動的に定義されるときは、適切に引数を渡すだけで構いません。

こうして、適切な場所にchild specを定義すれば、指定の間違いが避けられます。有効に活用していきましょう。

Top comments (0)