DEV Community

gumi TECH for gumi TECH Blog

Posted on • Updated on

 

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

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

スーパーバイザーは、とりあえず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
}
Enter fullscreen mode Exit fullscreen mode

スーパーバイザーが子の仕様をすべて得ると、子をひとつずつリストの順に開始します。始め方は仕様の: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
Enter fullscreen mode Exit fullscreen mode

これまでスーパーバイザーは子のプロセスに対して、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
Enter fullscreen mode Exit fullscreen mode

これでプロセスに名前が登録されます。コンパイルしたら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>}
Enter fullscreen mode Exit fullscreen mode

スーパーバイザーを起動すると、登録の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"}]}.
Enter fullscreen mode Exit fullscreen mode

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

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

あるいは、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]}
Enter fullscreen mode Exit fullscreen mode

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

第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
Enter fullscreen mode Exit fullscreen mode
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.134.0>}
Enter fullscreen mode Exit fullscreen mode

KV.Registryのプロセスは確かに使えました。けれど、KV.Registry.create/2GenServer.cast/2を呼び出しています。つまり、メッセージのターゲットのあるなしにかかわらず、ただちに:okが返されるということです。したがって、ここまでではスーパーバイザーやサーバーが働いて、プロセスがつくられたかどうかはわかりません。けれども、KV.Registry.lookup/2GenServer.call/3を使っています。つまり、サーバーの応答を待つのです。レスポンスが返ったということは、正しく動いていることを示します。

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

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

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

MixとOTPもくじ

Top comments (0)

An Animated Guide to Node.js Event Loop

Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

What happens under the hood when Node.js works on tasks such as database queries? We will explore it by following this piece of code step by step.