Edited at

Elixir 1.6 で入る DynamicSupervisor について

More than 1 year has passed since last update.

Elixir 1.6 で DynamicSupervisor というモジュールが入る予定です。


DynamicSupervisor とは

端的に言えば :simple_one_for_one を殺すためのモジュール です。

Supervisor モジュールの再起動戦略として :one_for_one, :rest_for_one, :one_for_all, :simple_one_for_one がありますが、この :simple_one_for_one の代替となる機能を DynamicSupervisor は提供します。

実際 Elixir 1.6 で :simple_one_for_one非推奨(deprecated) になります。1

Supervisor では child spec を書いて子と一緒に起動させることが多いですが、DynamicSupervisor は必ず子が空の状態で起動します。

起動後、DynamicSupervisor.start_child/2 で動的に追加する形になります。まさに :simple_one_for_one の機能です。


なぜ :simple_one_for_one を使わないのか

:simple_one_for_one が、Erlang のスーパーバイザの中でかなり特殊だからです。

スーパーバイザのドキュメントには「:simple_one_for_one でない場合は〜となる。:simple_one_for_one の場合は〜となる」というのが多くあり、スーパーバイザを理解するのが難しくなっています。

実際、例えば Supervisor.start_child/2 は、:simple_one_for_one 以外なら child spec を渡すのに、:simple_one_for_one の場合は起動する引数のリストを渡す必要があります。

また、:simple_one_for_one には各プロセスに対して一意な ID が不要で、そのために Supervisor.terminate_child/2 で PID しか指定できないし、Supervisor.delete_child/2 はサポートしていません。

この問題を解決するため、:simple_one_for_one を別のモジュールとして独立させて利用することにしたようです。2

実際、DynamicSupervisor を導入することによって、Supervisor のドキュメントは 結構スッキリしました


DynamicSupervisor を現実的な方法で利用する

簡単な利用方法は Elixir 1.6.0 の新機能の紹介 に書かれています。

DynamicSupervisor を起動するには、以下のように start_link/1 を使えばいいようです。

{:ok, sup} = DynamicSupervisor.start_link(strategy: :one_for_one)

ただ、実際にこういうコードを書くことは無く、大抵の場合はアプリケーションのスーパーバイザにぶら下げることになるでしょう。

ここでは、現実的に DynamicSupervisor を利用したコードを書くならどのようになるのかを説明します。

DynamicSupervisor をモジュールベースで利用するなら、以下のように書いてアプリケーションのスーパーバイザにぶら下げることになります。

defmodule MyApp.Client.Supervisor do

use DynamicSupervisor

def start_link([]) do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end

@impl DynamicSupervisor
def init([]) do
DynamicSupervisor.init(strategy: :one_for_one)
end
end

defmodule MyApp.Application do
use Application

def start(_type, _args) do
children = [
MyApp.Client.Supervisor,
]

Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
end

あるいは MyApp.Client.Supervisor モジュールを定義するのが面倒なら以下のように書くという手もあります。

defmodule MyApp.Application do

use Application

def start(_type, _args) do
children = [
%{
id: MyApp.Client.Supervisor,
start: {
DynamicSupervisor,
:start_link,
[[name: MyApp.Client.Supervisor, strategy: :one_for_one]]
}
},
]

Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
end

詳細については スーパーバイザの下にスーパーバイザをぶら下げる簡単な方法 を参照して下さい。この記事と同様の方法でスーパーバイザの下に DynamicSupervisor をぶら下げています。

これで DynamicSupervisor を起動できたので、あとは子を起動するだけです。

:simple_one_for_one では init/1 時に指定した1種類のモジュールしか起動できませんでしたが、DynamicSupervisor では 何種類のモジュールでも起動できます

が、通常は1種類しか使わないので、今回の例も1種類のモジュールだけでやります。

defmodule MyApp.Client do

use GenServer, restart: :temporary

def start_link([]) do
GenServer.start_link(__MODULE__, [])
end
end

# 子を生成
DynamicSupervisor.start_child(MyApp.Client.Supervisor, MyApp.Client)

DynamicSupervisor.start_child/2 の第一引数にはスーパーバイザの名前か pid を、第二引数には child spec を渡します。

モジュール名を渡せば、そのモジュールの child_spec/1 を呼び出して起動します。

詳細については Elixir 1.5 の合理化された child spec を参照して下さい。

use GenServer がデフォルトで定義する child_spec/1restart: :permanent になっているので、大体の場合は :temporary に変えておいた方がいいでしょう。

DynamicSupervisor の子は動的に大量に作ることになり、大抵の場合は再起動したら困るものが殆どになるはずです。

また、DynamicSupervisor の性質上、任意の文字列をキーにして子プロセスを検索したくなるということがあります。

例えばセッションならセッションID、何らかのルームならルームIDで pid を検索、といった具合です。

:one_for_one で作ったプロセスなら、大体 GenServer.start_link/3 時に name: __MODULE__ とか書いて名前を付けて利用するだけで済みますが、DynamicSupervisor では子を動的に作るので atom を動的に作ることになり、これは死ぬ未来しかありません。

そのため、子プロセスを整数や文字列で検索したくなるのです。

このような用途のために、丁度 Registry というライブラリがあります。

これを使えば、以下のように書けます。

defmodule MyApp.Client do

use GenServer, restart: :temporary

def start_link([client_id]) do
name = {:via, Registry, {MyApp.Client.Registry, client_id}}
GenServer.start_link(__MODULE__, [], name: name)
end
end

client_id = "..."

# 子プロセスを生成
DynamicSupervisor.start_child(MyApp.Client.Supervisor, {MyApp.Client, [client_id]})

# Registry を経由して client_id のプロセスを呼び出す
name = {:via, Registry, {MyApp.Client.Registry, client_id}}
value = GenServer.call(name, :get_value)

MyApp.Client.start_link/1 で、name の指定をよく分からない形式で書いていますが、これは Registry のドキュメントにある通りの書き方です。

このよく分からない形式は、GenServer.call/2 時にも使えて、このように書くことでプロセス ID や atom 形式の名前の代わりに利用できます。

詳細は後日書きます。→ @kenichirow が書いてくれました Registry の via_tuple について

なお、ここまで書くことになったら、MyApp.Client.Supervisor をモジュールベースにして、MyApp.Client.Supervisor.start_child(client_id) のように書いて構築できるようにしておいた方がいいでしょう。

また、MyApp.Client.get_value() を定義して、その中で GenServer.call/2 するようなコードにしておいた方が良いです。3

最終的に、DynamicSupervisor を利用した一般的なアプリケーションは、以下のように書くことになるでしょう。


lib/my_app/client.ex

defmodule MyApp.Client do

# restart: :temporary にする
use GenServer, restart: :temporary

# 一意な ID を受け取り Registry に登録して起動する
def start_link([client_id]) do
name = {:via, Registry, {MyApp.Client.Registry, client_id}}
GenServer.start_link(__MODULE__, %{value: 100}, name: name)
end

@impl GenServer
def handle_call(:get_value, _from, state) do
{:reply, state.value, state}
end

# Registry 経由で call する処理を自身のモジュールで提供する
def get_value(client_id) do
name = {:via, Registry, {MyApp.Client.Registry, client_id}}
GenServer.call(name, :get_value)
end
end



lib/my_app/client/supervisor.ex

defmodule MyApp.Client.Supervisor do

use DynamicSupervisor

def start_link([]) do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end

@impl DynamicSupervisor
def init([]) do
DynamicSupervisor.init(strategy: :one_for_one)
end

# 子を起動する処理を関数で提供する
def start_child(client_id) do
DynamicSupervisor.start_child(__MODULE__, {MyApp.Client, [client_id]})
end
end



lib/my_app/application.ex

defmodule MyApp.Application do

use Application

def start(_type, _args) do
children = [
MyApp.Client.Supervisor,
# MyApp.Client 用の Registry を起動しておく
{Registry, keys: :unique, name: MyApp.Client.Registry},
]

Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
end


これによって、以下のようにシンプルに利用できるようになります。

client_id = "..."

# 子プロセスを起動する
MyApp.Client.Supervisor.start_child(client_id)

# 子プロセスからデータを取得する
value = MyApp.Client.get_value(client_id)


まとめ

:simple_one_for_one のことは忘れて、Elixir 1.6 からは良い感じに DynamicSupervisor を活用していきましょう。


参考





  1. :simple_one_for_one は、非推奨になるしドキュメントからも消えるものの、機能としてはずっと残り続けます(OTP の機能なので)。 



  2. あとは GenStage で子の数を制限したりできるスーパーバイザが欲しかったというのもあったみたいだけれども、GenStage が Elixir に合流する予定が無くなったのでそこはあまり理由にならない 



  3. これは DynamicSupervisor とは関係なく、普通の抽象化の話です。