読者です 読者をやめる 読者になる 読者になる

OTPを勉強した(Supervisor)

Elixir

前回・前々回と同様、OTPについて公式ガイド見ながら学習した。 以下公式ガイドのコードを利用しつつ備忘録がてら書く

Supervisor

エラーが起きた場合、そのエラーをrescueしようとするのが普通のプログラミング言語 だが、elixirではlet it crashさせる。

そのlet it crashを支えるのがSupervisorであり、この仕組を使うことでcrashしたプロセスの監視、再起動などを行わせることができる。

まず最初にKV.Registryを監視するSupervisorを作成するために、lib/kv/supervisor.exにKV.Supervisorを定義する。

defmodule KV.Supervisor do
  use Supervisor

  def start_link do
    Supervisor.start_link(__MODULE__, :ok)
  end

  def init(:ok) do
    children = [
      worker(KV.Registry, [KV.Registry])
    ]
    supervise(children, strategy: :rest_for_one)
  end
end

仕組みとしてはinit/1内でworker(KV.Registry, [KV.Registry]) と設定すると、Supervisorが KV.Registry.start_link(KV.Registry) としてプロセスの立ち上げを行ってくれる。

start_linkには名前を渡しておくと便利で、またその名前もモジュール名と同名だと更にデバッグのときとかに便利だとのこと。

Application

mix compileなどでコンパイルすると_build/dev/lib/kv/ebin/*.appにmix管理下のApplication情報が保存されるようになってるみたい。

erlangVM上で利用される設定って認識で良いのだろうか。

この設定はmix.exsをいじることで変更できる。

Application behaviour

Applicationを起動したと同時にSupervisorも起動できるように設定する。

mix.exsを以下のように設定する。

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

:modにはApplicationが起動された際にcallbackとして渡されるmoduleを設定することができる。なお、この:modで指定するmoduleにはApplication behaviourを実装する必要がある。

今回は以下のようにlib/kv.exをcallback moduleとして設定する。

# lib/kv.ex
defmodule KV do
  use Application

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

これだけで、Applicationを実行した際にSupervisorを起動できる

$ iex -S mix
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.181.0>}

KV.Bucket.Supervisor

RegistryとBucketはお互いにリンクしているため、Bucketがcrashした場合、Registryもcrashしてしまう。 現状の作りとしてRegistry自体のバックアップ、再起動はできるが、Bucketのバックアップ・再起動はなされないためRegistryがcrashしたら中身を復元することができない。

それを解消するために、KV.Bucket.Supervisorを定義する必要がある。

defmodule KV.Bucket.Supervisor do
  use Supervisor

  @name KV.Bucket.Supervisor

  def start_link do
    Supervisor.start_link(__MODULE__, :ok, name: @name)
  end

  def start_bucket do
    Supervisor.start_child(@name, [])
  end

  def init(:ok) do
    children = [
      worker(KV.Bucket, [], restart: :temporary)
    ]
    supervise(children, strategy: :simple_one_for_one)
  end
end

この場合、RegistryでBucketを作成する際にもSupervisor経由で作成する必要があるのでlib/registry.exのhandle_cast/2を以下のように修正する。

def handle_cast({:create, name}, {names, refs}) do
  if Map.has_key?(names, name) do
    {:noreply, {names, refs}}
  else
    {:ok, pid} = KV.Bucket.Supervisor.start_bucket
    ref = Process.monitor(pid)
    refs = Map.put(refs, ref, name)
    names = Map.put(names, name, pid)
    {:noreply, {names, refs}}
  end
end

またこのBucketのSupervisorをApplication内で利用するためにlib/kv/supervisor.exも以下のように変更する必要がある

def init(:ok) do
  children = [
    worker(KV.Registry, [KV.Registry]),
    supervisor(KV.Bucket.Supervisor, [])
  ]
  supervise(children, strategy: :rest_for_one)
end

なお上記ではstrategyも変更している。

なぜならone_for_oneだと、KV.Registryがcrashした際にKV.Bucketの情報の全てが失われるためKV.Bucket.Supervisorがcrashし、KV.Bucketが孤立してしまう恐れがあるからだ。(ちょっとこのあたりの理解がふわふわしてる。)

そのため今回はrest_for_onestrategyに変更している。 これはある子プロセスが死んだ場合、その子プロセスよりもあとに起動される子プロセスを再起動させるものだ。

つまりこの場合、KV.Registryが死んだ場合、KV.Bucket.Supervisorが再起動されるが、逆にKV.Bucket.Supervisorが死んでもKV.Registryは再起動されない。

その他

  • iex -S mixの正体

When you run iex -S mix, it is equivalent to running iex -S mix run.

iex -S mix runが実行されてるみたい。初めて知った。

所感

  • strategyの辺りがふわふわとした理解なのでそのあたりのドキュメントを今度読もう。

参考