LoginSignup
2
0

More than 5 years have passed since last update.

Elixir Concurrent Programming(3) - Supervisors

Last updated at Posted at 2018-09-27

■ ElixirのConcurrent Programmingについてまとめた過去記事
Elixir Concurrent Programming(1) - Spawn - Qiita
Elixir Concurrent Programming(2) - GenServer - Qiita
Elixir Concurrent Programming(3) - Supervisors - Qiita
Elixir Concurrent Programming(4) - Task and Agent -Qiita

 OTP supervisor behaviorは、worker processを監視し、停止したプロセスを再起動する機能を提供してくれます。

 以下、公式サイトSupervisor behaviourで挙げられているコードを参考としつつ、Supervisorの機能を確認していきたいと思います。

1.プロジェクト作成

 stackプロジェクトを作成します。

mix new --sup stack

 --supが付いているので、application.exというファイルが生成されます。このファイルについては後で説明します。

# mix new --sup stack
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/stack.ex
* creating lib/stack/application.ex ### supオプションでこれが作成される
* creating test
* creating test/test_helper.exs
* creating test/stack_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd stack
    mix test

Run "mix help" for more commands.

 このプロジェクトで動作するプロセスは以下の3個になります。SupervisorがBackupとServerを起動し、監視し、停止時に再起動します。このプロセスの関係はapplication.exで定義します。

Supervisor ------ Backup
              |
              |-- Server

2.Stack.Server

 Stack.Serverは、Stackサービスを提供するGenServerです。initでStack.Backup.get()で初期化用のstateを取得します。terminate時にカレントのstateをStack.Backup.updateでバックアップします。強制的にプロセスを停止させるためにhandle_cast(:kill, stack)を実装しています。

lib/stack/server.ex
defmodule Stack.Server do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, nil, name: __MODULE__)
  end

  def pop do
    GenServer.call __MODULE__, :pop
  end

  def push(head) do
    GenServer.cast __MODULE__, {:push, head}
  end


  ## Callbacks

  def init(_) do
    {:ok, Stack.Backup.get() }  ### 初期化時にBackupサーバを参照
  end

  def handle_call(:pop, _from, [head | tail]) do
    {:reply, head, tail}
  end

  def handle_cast({:push, head}, tail) do
    {:noreply, [head | tail]}
  end

  def handle_cast(:kill, stack) do  ### 強制的にプロセスを停止させる
    a = 1 / 0
    {:noreply, [stack]}
  end

  def terminate(_reason, current_stack) do
    Stack.Backup.update(current_stack)  ### 停止時にBackupサーバを更新
  end
end

3.Stack.Backup

 Stack.Backup GenServerは、Stack.Serverがstackをバックアップするためのサーバです。Stack.Serverは初期値をStack.Backupから取得します。またStack.Serverは、停止時にStack.Backupにバックアップを取り、再起動時にそのバックアップを取得することで、停止時のstateのを復元します。

lib/stack/backup.ex
defmodule Stack.Backup do
  use GenServer
  @me __MODULE__

  def start_link(initial_stack) do
    GenServer.start_link(__MODULE__, initial_stack, name: @me)
  end

  def get() do
    GenServer.call(@me, { :get })
  end

  def update(new_stack) do
    GenServer.cast(@me, { :update, new_stack })
  end


  # Server implementation

  def init(initial_stack) do
    { :ok, initial_stack }
  end

  def handle_call({ :get }, _from, current_stack ) do
    { :reply, current_stack, current_stack }
  end

  def handle_cast({ :update, new_stack }, _current_stack) do
    { :noreply, new_stack }
  end
end

4.application.ex

 childrenに起動するプロセスをリストします。Stack.Backupには初期値を指定します。Stack.ServerはStack.Backupから初期値を得るので、初期値は指定しません。

 :rest_for_one戦略は次のようなものです。childrenリストのサーバが死んだときに、自分より後にリストされているサーバも停止する。その後で停止サーバを全て再起動する。その他の戦略は次の通りです。:one_for_one は単に死んだサーバを再起動するのみで、デフォルトです。:one_for_all はサーバが死んだら、他の全てのサーバも停止して、全て再起動する戦略です。

lib/stack/application.ex
defmodule Stack.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  def start(_type, initial_stack) do
    # List all child processes to be supervised
    children = [
        { Stack.Backup, initial_stack},
        { Stack.Server, nil},
    ]

    opts = [strategy: :rest_for_one, name: Stack.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

 OTP Applicationの実装です。mix.exsにて本ApplicationのトップのmoduleがStackであることを教え、初期値を与えます。registeredオプションでStack.ServerやStack.Backupの名前のユニーク性(Node単位)を確保します。

mix.exs
  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger],
      mod: {Stack.Application, [[:hello]]},
      registered: [
        Stack.Server,
        Stack.Backup,
      ]
    ]
  end

5.Supervisorの動作の確認

 以下、Stack.Serverがエラーを起こし停止します。自動的に再起動するのですが、停止した時のstateを保持しているのがわかります。後ろでStack.Backupが機能しているおかげです。

# iex -S mix
Generated stack app
iex(1)> Stack.Server.pop
[:hello]
iex(2)> Stack.Server.push(:world)
:ok
iex(3)> Stack.Server.push(:world2)
:ok
iex(4)> Stack.Server.push(:world3)
:ok
iex(5)>  GenServer.cast Stack.Server, :kill
:ok
iex(6)>
12:56:07.504 [error] GenServer Stack.Server terminating
** (ArithmeticError) bad argument in arithmetic expression
    (stack) lib/stack/server.ex:32: Stack.Server.handle_cast/2
    (stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
    (stdlib) gen_server.erl:686: :gen_server.handle_msg/6
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:"$gen_cast", :kill}
State: [:world3, :world2, :world]

nil
iex(7)> Stack.Server.pop
:world3
iex(8)> Stack.Server.pop
:world2

 以上です

OTP supervisor behavior関連の過去記事
東京電力電力供給状況監視 - Phoenix Channel - Qiita
Phoenix Channelで作る最先端Webアプリ - Elixier Application編 - Qiita
Phoenix Channelで作る最先端Webアプリ - DynamicSupervisor編 - Qiita

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0