Elixir で GenServer, Supervisor, mix deps.get で取得したパッケージの Application の起動順について調べたのでそのまとめ。
GenServer
GenServer.start_link/3
のドキュメントに次のように記述されています。
To ensure a synchronized start-up procedure, this function does not return until init/1
has returned.
Genserver.start_link/3
と、いうわけで。同じプロセスで複数の GenServer プロセスを起動するばあい、先に起動したプロセスの init/1
が完了してから後続のプロセスが起動することが保証できます。先に起動したプロセス宛に安全にメッセージを送信できると考えてよさそうです。
例
メッセージを受信する GenServer 。
defmodule Hoge.Foo do
use GenServer
require Logger
def start_link(), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state), do: {:ok, state}
def handle_cast({:foo, sender}, state) do
Logger.info("#{__MODULE__} has received a message from #{sender}")
{:noreply, state}
end
end
メッセージを送信する GenServer 。
defmodule Hoge.Bar do
use GenServer
def start_link(opts) do
receiver = get_in(opts, [:receiver])
GenServer.start_link(__MODULE__, %{receiver: receiver})
end
def init(state) do
GenServer.cast(state.receiver, {:foo, __MODULE__})
{:ok, state}
end
end
メッセージを受信する GenServer と送信する GenServer を順に起動します。
defmodule Hoge do
def do_something do
{:ok, foo_pid} = Hoge.Foo.start_link([])
{:ok, bar_pid} = Hoge.Bar.start_link(receiver: foo_pid)
end
end
実行。
$ iex -S mix
iex(1)> Hoge.do_something()
18:21:48.782 [info] Elixir.Hoge.Foo has received a message from Elixir.Hoge.Bar
この例ではほとんど処理時間がないためわかりにくいですが、Foo.init/1
で時間がかかる処理を記述してみても完了するまで Foo.start_link/1
は処理を待つので、Bar
は安全にメッセージを Foo
宛に送信することができます。
Supervisor
Supervisor
のドキュメントに次のように記述されています。
When the supervisor starts, it traverses all child specifications and then starts each child in the order they are defined.
start and shutdownSupervisor
また終了についても同じページの同じセクションに次のようにあります。
The shutdown process happens in reverse order.
監督する子プロセスの定義順に起動し、終了時はその逆順で停止されるようです。
例
Hoge
を次のように書き換えます。
defmodule Hoge do
def do_something do
children = [
Hoge.Foo,
{Hoge.Bar, receiver: Hoge.Foo}
]
opts = [strategy: :one_for_one, name: Hoge.Supervisor]
Supervisor.start_link(children, opts)
end
end
Hoge.Foo
は名前付きで起動しているので GenServer.cast/2
は名前で呼び出すことができます。それを利用して、Hoge.Bar
には送信先を指定するパラメータに名前を渡しています。
実行。
$ iex -S mix
iex(1)> Hoge.do_something
18:32:38.261 [info] Elixir.Hoge.Foo has received a message from Elixir.Hoge.Bar
特に代わり映えはしないです、はい。
mix deps.get で取得した Application
例えば次のように Foo
と Bar
がアプリケーションで、Hoge
がそれらを取り込んでなおかつ起動順を指定したいばあいです。
./
├── bar/
│ └── lib/
│ ├── bar/
│ │ └── application.ex
│ └── bar.ex
├── foo/
│ └── lib/
│ ├── foo/
│ │ └── application.ex
│ └── foo.ex
└── hoge/
├── lib/
│ ├── hoge/
│ │ └── application.ex
│ └── hoge.ex
└── mix.exs
このようなばあいは、まず、 application/0
が返すパラメータに :included_application
を追加してそこにアプリケーション名を記述します。
アプリケーション名は mix.exs
の project/0
に :app
で記載しているパラメータの値になります。モジュール名でないので注意してください。
defmodule Hoge.MixProject do
use Mix.Project
def project do
[
app: :hoge,
version: "0.1.0",
elixir: "~> 1.8",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger],
included_applications: [:foo, :bar],
mod: {Hoge.Application, []}
]
end
defp deps do
[
{:foo, path: "../foo"},
{:bar, path: "../bar"}
]
end
end
ドキュメントにある通り :included_applications
に記載されたアプリケーションは自動的には起動しなくなります。
Any included application, defined in the :included_applications
key of the .app
file will also be loaded, but they won't be started.
*Application.start/2
取り込んだ側のアプリケーションで Supervisor
などを利用して起動することで、アプリケーションの起動順を制御することができます。
たとえば。Foo.Application
と Bar.Application
を次のように定義しておくと、
defmodule Foo.Application do
use Application
def start(, ) do
children = [
Foo
]
opts = [strategy: :one_for_one, name: Foo.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule Bar.Application do
use Application
def start(, args) do
children = [
{Bar, args}
]
opts = [strategy: :one_for_one, name: Bar.Supervisor]
Supervisor.start_link(children, opts)
end
end
Hoge.Application
に次のように記述することで、順序を指定して起動させることができるようになります。
defmodule Hoge.Application do
use Application
def start(type, ) do
children = [
%{id: Foo.Application, start: {Foo.Application, :start, [type, nil]}},
%{id: Bar.Application, start: {Bar.Application, :start, [type, [receiver: Foo]]}}
]
opts = [strategy: :one_for_one, name: Hoge.Supervisor]
Supervisor.start_link(children, opts)
end
end
子プロセスを定義する記述は、 Supervisor.start_link/2
のエラーメッセージに書かれた説明が一番わかりやすそうです。
If you own the given module, please define a child_spec/1 function
that receives an argument and returns a child specification as a map.
For example:
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
Note that "use Agent", "use GenServer" and so on automatically define
this function for you.
However, if you don't own the given module and it doesn't implement
child_spec/1, instead of passing the module name directly as a supervisor
child, you will have to pass a child specification as a map:
%{
id: Foo.Application,
start: {Foo.Application, :start_link, [arg1, arg2]}
}
メッセージにあるようにモジュールに child_spec/1
と定義するか、:id
と :start
を持つ Map を渡すかするようにします。
上記の例では Map を渡しています。
いつか読むはずっと読まない: Futabasaurus suzukii
国内で最も有名な首長竜にも関わらず正式な学名がついたのは 2006 年。
「かはく」こと 国立科学博物館の日本館三階北翼 をぜひ訪ねてみてください。