MixとOTP 04: スーパーバイザーとアプリケーション
gumi TECH
Posted on April 2, 2019
本稿はElixir公式サイトの許諾を得て「Supervisor and Application」の解説にもとづき、加筆補正を加えて、ElixirにおけるSupervisor
の使い方とアプリケーションの扱い方についてご説明します。
ソフトウェアに失敗が起こったとき、大抵はそれを「回復しよう」とするでしょう。けれど、Elixirでは例外を解消するような受け身のプログラミングはしません。むしろ、「落ちるに任せる」のです。プロセスがバグでクラッシュしても、心配には及びません。スーパーバイザーを定めて、プロセスの新しいコピーを代わりに動かせばよいからです。
はじめてのスーパーバイザー
Supervisor
のつくり方は、GenServer
とほぼ変わりません。ただし、使うビヘイビアはSupervisor
です。新たなファイルlib/kv/supervisor.ex
に、モジュールKV.Supervisor
をつぎのように定めてください。
defmodule KV.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, :ok, opts)
end
def init(:ok) do
children = [
KV.Registry
]
Supervisor.init(children, strategy: :one_for_one)
end
end
スーパーバイザーは、とりあえずKV.Registry
をひとつ子にもちます。子のリストを定めたら、Supervisor.init/2
にリストと監視戦略を渡して呼び出します。監視戦略は、子のいずれかがクラッシュしたときどうするかという指示です。:one_for_one
というのは、子どもがひとつ落ちたら、その子どもを、ひとつだけ再起動するということです。今のところ子どもはひとつですのでこれでよいでしょう。Supervisor
ビヘイビアには、ほかにも多くの戦略が備わっています。
スーパーバイザーが動き出すと、リストに納められた子のすべてに対して、各モジュールのchild_spec/1
関数を呼び出します。戻り値は名前のとおり、子の仕様です。プロセスの始まり方や、プロセスがワーカーなのかスーパーバイザーなのか、プロセスが仮か一時的か永続的かといったことが示されます。たとえば、つぎのような情報です。child_spec/1
関数は、Agent
かGenServer
あるいはSupervisor
を使うと自動的に定められます。
iex> KV.Registry.child_spec([])
%{
id: KV.Registry,
restart: :permanent,
shutdown: 5000,
start: {KV.Registry, :start_link, [[]]},
type: :worker
}
スーパーバイザーが子の仕様をすべて得ると、子をひとつずつリストの順に開始します。始め方は仕様の:start
キーに示されており、上の例はKV.Registry.start_link([])
を呼び出すということです。
「MixとOTP 03: GenServer」では、テストのときプロセスをstart_supervised!/2
関数により始めました。start_supervised!/2
は内部的に、ExUnit
フレームワークが定めたスーパーバイザーにもとづいてプロセスを開始しているのです。スーパーバイザーを独自に定義すれば、アプリケーションの初期化や終了、あるいは監視の登録などを構成して、最終的なコードやテストを最適に調整できます。
今のところstart_link/1
は、オプションとしてつねに空のリストを受け取ります。
プロセスに名前をつける
アプリケーションは多くのプロセスをつくります。けれど、それらを登録するKV.Registry
はひとつです。そこで、KV.Registry
はPIDで参照するのでなく、名前をつけましょう。そうすれば、つねに名前で参照が得られます。
つくられるプロセスは、ユーザーの入力にもとづいて動的に開始されました。ですから、プロセスを管理するのに、アトムの名前はつけるべきではありません。登録するKV.Registry
は違います。アプリケーションが起動するとき、スーパーバイザーのもとでひとつだけ開始されるのです
では、前掲モジュールKV.Supervisor
(lib/kv/supervisor.ex
)で定めたスーパーバイザーの子は、タプルのリストになるように少し手直ししましょう。
def init(:ok) do
children = [
# KV.Registry
{KV.Registry, name: KV.Registry}
]
Supervisor.init(children, strategy: :one_for_one)
end
これまでスーパーバイザーは子のプロセスに対して、KV.Registry.start_link([])
を呼び出していました。それが修正によって、KV.Registry.start_link([name: KV.Registry])
の呼び出しに変わったのです。KV.Registry.start_link/1
の実装はつぎのとおりでした。このGenServer.start_link/3
に渡される第3引数のオプションに、:name
として上の手直しで加えた名前が渡されるのです(「Name registration」参照)。
def start_link(opts) do
GenServer.start_link(__MODULE__, :ok, opts)
end
これでプロセスに名前が登録されます。コンパイルしたらiex -S mix
のシェルで試してみましょう。KV.Registry.lookup/2
の第1引数に、登録した名前が使えます。
iex> KV.Supervisor.start_link([])
{:ok, #PID<0.130.0>}
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.134.0>}
スーパーバイザーを起動すると、登録のKV.Registry
は名前が与えられて自動的に始まります。登録するプロセスも、つくれば開始されるのです。
実際には、アプリケーションのスーパーバイザーを開始する処理は滅多に書きません。アプリケーションのコールバックの一部として起動するからです。
アプリケーションを理解する
これまでアプリケーションのコードを書いて動かしてきました。コードに手を加えるたびに、ファイルはコンパイルしなければなりません。そのとき、_build/dev/lib/kv/ebin/kv.app
が書き出され、開くとつぎのような記述が見られます。
{application,kv,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"kv"},
{modules,['Elixir.KV','Elixir.KV.Bucket','Elixir.KV.Registry',
'Elixir.KV.Supervisor']},
{registered,[]},
{vsn,"0.1.0"}]}.
Erlangの構文で設定が書かれています。Erlangを知らなくても、アプリケーションの仕様であることが推測できるでしょう。アプリケーションのバージョンや定められているモジュール、さらに依存するアプリケーション(Erlangのkernel
とstdlib
、elixir
自身およびlogger
)などが示されています。
新たなモジュールを加えるたびに、このファイルに書き加えるのは面倒です。代わりにMixがファイルをつくり、更新してくれます。また、mix.exs
プロジェクトファイルにapplication/0
を定めることにより、カスタマイズすることもできるのです(「The application environment」参照)。
アプリケーションを起動する
.app
ファイルが定められると、その仕様にしたがってアプリケーションを全体として起動し、終了することができます。
- Mixはアプリケーションを自動的に起動できます。
- Mixが起動しなかったときは、アプリケーションは開始するまで何もしません。
試しに、Mixでアプリケーションを起動してみましょう。iex -S mix
でプロジェクトコンソールを開いてください。
アプリケーションはApplication.start/2
で始まり、Application.stop/1
で終えられます。デフォルトでは、プロジェクトのmix.exsが定めるアプリケーション階層の全体を、Mixが自動的に開始しています。他に依存しているアプリケーションがあれば、それらについても同様です。
iex> Application.start(:kv)
{:error, {:already_started, :kv}}
iex> Application.stop(:kv)
00:00:00.000 [info] Application kv exited: :stopped
:ok
iex> Application.start(:kv)
:ok
Mixにオプションを渡して、アプリケーションは起ち上げないこともできます。iex -S mix run --no-start
と打ち込んでコンソールを開いてください。
Application.start/2
でアプリケーションが開始します。:logger
はElixirがデフォルトで起動するアプリケーションです。依存するアプリケーションを停止すると、再起動するにはそれを先に起ち上げなければなりません。
iex> Application.start(:kv)
:ok
iex> Application.stop(:kv)
00:00:00.000 [info] Application kv exited: :stopped
:ok
iex> Application.stop(:logger)
=INFO REPORT==== 20-Aug-2018::00:00:00.000000 ===
application: logger
exited: stopped
type: temporary
:ok
iex> Application.start(:kv)
{:error, {:not_started, :logger}}
iex> Application.start(:logger)
:ok
iex> Application.start(:kv)
:ok
あるいは、Application.ensure_all_started/2
で依存しているアプリケーションとともに起動できます。
iex> Application.stop(:kv)
00:00:00.000 [info] Application kv exited: :stopped
:ok
iex> Application.stop(:logger)
=INFO REPORT==== 20-Aug-2018::00:00:00.000000 ===
application: logger
exited: stopped
type: temporary
:ok
iex> Application.ensure_all_started(:kv)
{:ok, [:logger, :kv]}
iex -S mix
はiex -S mix run
の省略記法です。オプションを加えるときは、run
コマンドのあとに添えなければならず、省けません。run
コマンドと使えるオプションについてはmix help run
で確かめられます。
アプリケーションコールバック
アプリケーションの起動について知ると、何ができるでしょうか。ひとつ挙げられるのは、application
コールバック関数を定めることです。コールバックはアプリケーションが起ち上がるときに呼び出されます。関数は{:ok, pid}
を返さなければなりません。pid
はスーパーバイザープロセスの識別子です。
アプリケーションコールバックは、ふたつの手順で組み立てられます。第1に、mix.exs
の関数application
に:mod
オプションで、アプリケーションコールバックモジュールを加えます。タプルの第1要素がモジュール、第2要素はアプリケーション起動時に渡される引数です。アプリケーションコールバックモジュールは、Application
ビヘイビアを実装していればどれでもかまいません。
def application do
[
extra_applications: [:logger],
mod: {KV, []}
]
end
第2に、アプリケーションコールバックモジュールにコールバック関数を定めます。アプリケーションの開始時に呼び出されるのがstart/2
です。終了時に呼び出したいコールバックはstop/1
として加えてください。ここでは、プロジェクトにデフォルトでつくられたモジュールKV
のlib/kv.ex
をつぎのように書き替えます。モジュールにuse Application
を忘れずに加えてください。
プロジェクトをコンパイルしたら、iex -S mix
でコンソールを開きます。すると、KV.Registry
はすでに動いていることが確かめられるでしょう。
defmodule KV do
use Application
def start(_type, _args) do
KV.Supervisor.start_link(name: KV.Supervisor)
end
end
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.134.0>}
KV.Registry
のプロセスは確かに使えました。けれど、KV.Registry.create/2
はGenServer.cast/2
を呼び出しています。つまり、メッセージのターゲットのあるなしにかかわらず、ただちに:ok
が返されるということです。したがって、ここまでではスーパーバイザーやサーバーが働いて、プロセスがつくられたかどうかはわかりません。けれども、KV.Registry.lookup/2
はGenServer.call/3
を使っています。つまり、サーバーの応答を待つのです。レスポンスが返ったということは、正しく動いていることを示します。
プロジェクトとアプリケーション
Mixはプロジェクトとアプリケーションを区別します。mix.exs
の内容にもとづいてつくられるのが、アプリケーションの定められたMixプロジェクトです。けれど、アプリケーションが含まれないプロジェクトもあります。
プロジェクトはMixでつくります。Mixはプロジェクトを管理するツールです。プロジェクトをコンパイルしたり、テストすることなどができます。さらに、関連するアプリケーションのコンパイルや起動もできるのです。アプリケーションというとき考えるのはOTPです。アプリケーションは、ランタイムが起動し、終了するもの全体を指します。
MixとOTPもくじ
Posted on April 2, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.