loading...
gumi TECH Blog

MixとOTP 04: スーパーバイザーとアプリケーション

gumitech profile image gumi TECH Updated on ・3 min read

本稿は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関数は、AgentGenServerあるいは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のkernelstdlibelixir自身およびlogger)などが示されています。

新たなモジュールを加えるたびに、このファイルに書き加えるのは面倒です。代わりにMixがファイルをつくり、更新してくれます。また、mix.exsプロジェクトファイルにapplication/0を定めることにより、カスタマイズすることもできるのです(「The application environment」参照)。

アプリケーションを起動する

.appファイルが定められると、その仕様にしたがってアプリケーションを全体として起動し、終了することができます。

  1. Mixはアプリケーションを自動的に起動できます。
  2. 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 mixiex -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として加えてください。ここでは、プロジェクトにデフォルトでつくられたモジュールKVlib/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/2GenServer.cast/2を呼び出しています。つまり、メッセージのターゲットのあるなしにかかわらず、ただちに:okが返されるということです。したがって、ここまでではスーパーバイザーやサーバーが働いて、プロセスがつくられたかどうかはわかりません。けれども、KV.Registry.lookup/2GenServer.call/3を使っています。つまり、サーバーの応答を待つのです。レスポンスが返ったということは、正しく動いていることを示します。

プロジェクトとアプリケーション

Mixはプロジェクトとアプリケーションを区別します。mix.exsの内容にもとづいてつくられるのが、アプリケーションの定められたMixプロジェクトです。けれど、アプリケーションが含まれないプロジェクトもあります。

プロジェクトはMixでつくります。Mixはプロジェクトを管理するツールです。プロジェクトをコンパイルしたり、テストすることなどができます。さらに、関連するアプリケーションのコンパイルや起動もできるのです。アプリケーションというとき考えるのはOTPです。アプリケーションは、ランタイムが起動し、終了するもの全体を指します。

MixとOTPもくじ

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide