技術メモ

プログラミングとか電子工作とか

Elixir入門(Mix and OTP編 第4章 Supervisor and Application)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
前回に引き続き第4章。リンクとモニターどっちかで、今回はモニター使うんだからリンクしないようにしよう!みたいな流れになる気がします。

そもそも発行したプロセスを監視しないといけないのはプロセスが何らかの不具合でクラッシュしたりするのを監視して再起動したりする処理が書きたいがため。
他の言語ならtry/catchの用に例外処理してもいいのかもしれませんが、Elixirでは基本的に推奨されてません。
そこで訳に立ってくるのがsupervisorだそうです。その機能を見ていきます。

Our first supervisor

supervisorの作成はGenServerとほぼ変わらないそうです。とりあえずサンプルを追っていきます。
supervisorのために、lib/kv/supervisor.exKV.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

分かるような分からないような・・・
start_link/1ではsupervisorの起動。おそらく第一引数にコールバック関数があるモジュールの指定、第二引数に初期化関数への引数を渡す感じでしょうか。あと、必要に応じてオプション(opts)。
初期化関数init/1では、childrenというリストにKV.Registryを含むリストを作成。おそらくこのリストにsupervisorで管理したいプロセスのモジュールを書くのだとと思います。
そのあとでSupervisor.init/2。第一引数は管理したいモジュールのリスト、第二引数は管理したモジュールがクラッシュした場合などの振る舞いのようです。
今回は:one_for_oneということで、各プロセスを独立して管理する方針でしょうか。

また、use Agentuse GenServeruse Supervisorを使っている場合はモジュールにchild_spec/1という関数が追加されます。一応iex.bat -S mixで実行してみます。

PS > iex.bat -S mix
Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> KV.Registry.child_spec([])
%{id: KV.Registry, start: {KV.Registry, :start_link, [[]]}}

なんかサンプルと違う・・・
とりあえず関数は追加されていて実行できるのでヨシとしましょう。

ちょっとまだ謎が多いですが、先に進みます。

Naming processes

GenServerで作成したレジストリもいちいちプロセスIDで管理せず名前を付けたいところです。
ここで、Bucketのとき同様アトムの使用を避けるのか?という発想になりかねませんが、このレジストリBucketを束ねるものですので、本質的に1個で良いはずです。
ですので、この場合はアトムで命名していきます。

  def init(:ok) do
    children = [
#      KV.Registry
      {KV.Registry, name: KV.Registry}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

{モジュール名, name: 名前}のタプルになってます。モジュール名やん!と思われるかもしれませんが、Elixirでコンパイルをするときはモジュール名はアトムへ書き換えられるのでname:の後は実質アトムと考えて問題無いと思います。
このchildrenの所に{モジュール名, name: 名前}でモジュールを登録しておくと、そのモジュールのstart_link/1の引数(opts)に[name: 名前]のキーワードリストが放り込まれて呼び出されるようです。
つまり、supervisorがKV.RegistryKV.Registry.start_link([name: KV.Registry])で呼び出し(プロセス発行し)ます。

ここまでできたらiexで実行してみます。iex.bat -S mixで起動です。(もしかしたらmix compileしといたほうがいいかも?)

PS > iex.bat -S mix
Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> KV.Supervisor.start_link([])
{:ok, #PID<0.129.0>}
iex(2)> KV.Registry.create(KV.Registry, "shopping")
:ok
iex(3)> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.133.0>}

KV.RegistryBucketを作成できてるので、supervisorを発行しただけでKV.Registryが立ち上がっている事が分かります。
本来はKV.RegistryのプロセスIDを追っていかないとBucketの作成はできませんが、KV.Registryというアトムで呼び出せるようになってるので自然に使えてます。この辺はsupervisor{KV.Registry, name: KV.Registry}とした旨みでしょうか。

今回はiexで実行したので手動でsupervisorを呼び出しましたが、アプリケーションのコールバックとして登録することで自動的に起動するよう設定するのが一般的だそうです。
その方法を追っていきます。

Understanding applications

コンパイルする度にGenerated kv appとかでてましたが、まずはその本体を見てみましょう。
_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"},
              {extra_applications,[logger]}]}.

たぶん依存関係とかが書かれたファイルでしょう。
アプリケーションのバージョンやmix.exsに書かれていた依存関係などが記載されています。
このファイルはErlangVMで起動するのに使われてるのでしょうが、mixが良い感じに出力してくれるようです。

なので、この依存関係や最初にスタートするスクリプトの設定などはmix.exsを編集すれば良いはずです。
mix.exsapplication/0という関数を調整していくようです。

Starting applications

.appファイルに設定が書かれていればApplicationモジュールを通してアプリケーションの開始や停止を制御できるようです。
基本的にはiex -S mixなどで起動すれば自動でアプリケーションが立ち上がってますし、立ち上がって無くても自分で開始したり停止したりできます。

iex -S mixで立ち上げて、起動していることを確認します。

iex> Application.start(:kv)
{:error, {:already_started, :kv}}

デフォルトで:kvが起動してます。(mix.exsproject/0で定義したnameがついてる?)
iex -S mix run --no-startiexを起動させればアプリケーションの起動をしないでiexが立ち上がるそうです。

PS > iex.bat -S mix run --no-start
Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Application.start(:kv)
:ok

なるほど。
Application.stop/1 でアプリケーションの停止ができます。

iex> Application.stop(:kv)
:ok
iex> Application.stop(:logger)
:ok

上では:loggerも止めましたが、:kvだけ起動してみます。

iex> Application.start(:kv)
{:error, {:not_started, :logger}}

:logger起動しとらんやんけ!と怒られてます。きちんと依存関係が確認されているようです。
Application.ensure_all_started/1を使えば、そのへんひっくるめて起動できるようです。

iex> Application.ensure_all_started(:kv)
{:ok, [:logger, :kv]}

まぁ手動で起動することはあんまりないんじゃないでしょうか。
まだElixirで開発進めてないのでなんともいえませんが・・・

The application callback

Elixirのアプリケーションが起動した際に起こっている事を少々理解したところで、supervisorの話に戻りましょう。
アプリケーションが起動した際に呼び出されるコールバック関数をモジュールに定義できるそうです。
start/2という関数で定義しておき、返値に{:ok, pid}が変えればいいようです。

まずは、コールバック関数があり呼び出しが必要である事をmix.exsに追記します。

  def application do
    [
      extra_applications: [:logger],
      mod: {KV, []} # これ
    ]
  end

:modというオプションを追加するとKVモジュールのstart/2関数が呼び出されるようです。
ドキュメントを読んでもsupervisorに使用してるようなのでsupervisor使うときの必須設定みたいなもんでしょうか。

KVモジュールにstart/2を追記します。

defmodule KV do
  use Application

  def start(_type, _args) do
    KV.Supervisor.start_link(name: KV.Supervisor)
  end
end

use Applicationしてるときは開始時のstart/2と停止時のstop/1をコールバック関数として実装できるようです。
今回はとりあえずstart/2だけ。

実装できたら実際に起動してるか確認します。
KV.Supervisor.start_link([])をした後でKV.Registry.createしてましたが、supervisorが起動してるならいきなりKV.Registry.createしてもいいはず。

PS > iex.bat -S mix
Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> KV.Registry.create(KV.Registry, "shopping")
:ok
iex(2)> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.133.0>}

できた!
この仕組みを理解してればいちいちSupervisorやGenServerの起動を意識して使用することは無さそうです。

イマイチ「OTPとは?」という回答にたどりつけてませんが、どうやらこのGenServerやSupervisorを使って機能を実現していくことあたりを指してる気がします。
まだまだ序盤なので先に進んでいきましょう。