Phoenix で Supervisor の Worker を作ってアクセスしてみる

Elixir の OTP まわりの復習として、Phoenix でプロジェクトを作成した時に作られる Supervisor ツリーに GenServer で作った Worker も入れてみて、動作を確認してみました。

最終的なソースコードは GitHub の[phoenix-studies-genserver](https://github.com/hykw/phoenix-studies-genserver)にあります。

# Phoenix のファイル準備
特に特別なところはありません。brunch も ecto も必要ないので、

```bash
mix phoenix.new study --no-brunch --no-ecto
```

で生成したものを使っています（※テスト環境の4000ポートは埋まっているので、`config/dev.exs` の port は5000に変更してあります）。

# Worker を GenServer で作成
`lib/study/counter.ex` として、下記のようなモジュールを作成しました。

get_state/0 を呼び出すと状態の値を、inc/0 はカウントを+1, また kill/0 は該当する handle_call/2 が無いため、パターンマッチエラーになることを期待しています。

```elixir
defmodule Study.Counter do
  use GenServer

  def start_link(), do: start_link(0)

  def start_link(init_num) do
    GenServer.start_link(__MODULE__, %{last_message: "初期状態", count: init_num}, name: __MODULE__)
  end

  def get_state do
    GenServer.call(__MODULE__, :get_state)
  end

  def inc do
    GenServer.cast(__MODULE__, :inc)
  end

  def kill do
    GenServer.cast(__MODULE__, :kill)
  end


  ####################
  def handle_call(:get_state, _from, state) do
    {:reply, state, state}
  end

  def handle_cast(:inc, state) do
    new_state = %{count: state.count+1, last_message: "inc呼び出し"}
    {:noreply, new_state}
  end

end
```

## 動作テスト
`iex -S mix` で、作成した GenServer が正しく動くことを確認します。

```ex
iex(1)> Study.Counter.start_link
{:ok, #PID<0.175.0>}

iex(2)> Study.Counter.get_state
%{count: 0, last_message: "初期状態"}

iex(3)> Study.Counter.inc
:ok

iex(4)> Study.Counter.get_state
%{count: 1, last_message: "inc呼び出し"}

iex(5)> Study.Counter.inc
:ok

iex(6)> Study.Counter.get_state
%{count: 2, last_message: "inc呼び出し"}

iex(7)> Study.Counter.kill
** (EXIT from #PID<0.173.0>) an exception was raised:
    ** (FunctionClauseError) no function clause matching in Study.Counter.handle_cast/2
        (study) lib/study/counter.ex:28: Study.Counter.handle_cast(:kill, %{count: 2, last_message: "inc呼び出し"})
        (stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
        (stdlib) gen_server.erl:681: :gen_server.handle_msg/5
        (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> [error] GenServer Study.Counter terminating
** (FunctionClauseError) no function clause matching in Study.Counter.handle_cast/2
    (study) lib/study/counter.ex:28: Study.Counter.handle_cast(:kill, %{count: 2, last_message: "inc呼び出し"})
    (stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
    (stdlib) gen_server.erl:681: :gen_server.handle_msg/5
    (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:"$gen_cast", :kill}
State: %{count: 2, last_message: "inc呼び出し"}

nil
iex(2)> Study.Counter.get_state
** (exit) exited in: GenServer.call(Study.Counter, :get_state, 5000)
    ** (EXIT) no process
    (elixir) lib/gen_server.ex:596: GenServer.call/3
```

また、Study.Counter.kill がクラッシュすることや、クラッシュ後にプロセスが死んでいることも確認しています。

# Supervisor の準備
作成した Worker をスーパーバイザ管理下にするため、`lib/study.ex` の children の worker の値を下記のように変更します。[0] はカウントの初期値です。

```ex
     children = [
        # Start the endpoint when the application starts
        supervisor(Study.Endpoint, []),
        # Start your own worker by calling: Study.Worker.start_link(arg1, arg2, arg3)
        # worker(Study.Worker, [arg1, arg2, arg3]),
        worker(Study.Counter, [0])  # ← この行を追加
      ]
```

## Supervisor 配下の Worker の再起動を確認
Supervisor により、Worker がクラッシュしても再起動されることを確認します。再度 `iex -S mix` で試してみます。

最初の起動もスーパーバイザが自動的に行なってくれるため、Study.Counter.start_link/0 を実行すると `:already_started` でエラーになることに注意してください（つまり、start_link を実行する必要は無い、ということです）。

```ex
iex(1)> Study.Counter.start_link
{:error, {:already_started, #PID<0.225.0>}}

iex(2)> Study.Counter.get_state
%{count: 0, last_message: "初期状態"}

iex(3)> Study.Counter.inc
:ok

iex(4)> Study.Counter.get_state
%{count: 1, last_message: "inc呼び出し"}

iex(5)> Study.Counter.kill
:ok

iex(6)> [error] GenServer Study.Counter terminating
** (FunctionClauseError) no function clause matching in Study.Counter.handle_cast/2
    (study) lib/study/counter.ex:28: Study.Counter.handle_cast(:kill, %{count: 1, last_message: "inc呼び出し"})
    (stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
    (stdlib) gen_server.erl:681: :gen_server.handle_msg/5
    (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:"$gen_cast", :kill}
State: %{count: 1, last_message: "inc呼び出し"}

nil
iex(7)> Study.Counter.get_state
%{count: 0, last_message: "初期状態"}
```

# Phoenix から呼び出し
Worker の動作、スーパーバイザによる再起動の動作確認ができましたので、Phoenix から Worker を呼び出してみます。と言っても、コントローラで関数を呼び出して、テンプレートにセットしているだけですが。

`router.ex` に、Worker クラッシュ用の `kill` を追加します。

```ex:page_controller.ex
  scope "/", Study do
      pipe_through :browser # Use the default browser stack

      get "/", PageController, :index
      get "/kill", PageController, :kill
    end
```

`controllers/page_controller.ex` および `templates/page/index.html.eex` は以下です。

index では現在の状態を取得して inc/0 を呼び出しています。また kill はパターンマッチ該当無しでクラッシュさせて、/ にリダイレクトしています。

```ex
  defmodule Study.PageController do
    use Study.Web, :controller

    def index(conn, _) do
      params = Study.Counter.get_state
      call_inc(Study.Counter)

      render(conn, "index.html", params)
    end

    defp call_inc(process_name) do
      process_name.inc
    end


    def kill(conn, _) do
      Study.Counter.kill()
      url = page_path(conn, :index)
      redirect(conn, to: url)
    end

  end
```

```ex:index.html.eex
  <div class="jumbotron">
    <h2><%= gettext "Welcome to %{name}", name: "Phoenix!" %></h2>
    <p class="lead">A productive web framework that<br />does not compromise speed and maintainability.</p>
  </div>

  <div class="row marketing">
    <div class="col-lg-6">
      <h4>Resources</h4>
      <ul>
        <li> 最終メッセージ：<%= @last_message %>
        <li> カウント数：<%= @count %>
      </ul>
    </div>

  </div>

  <a href="<%= page_path(@conn, :kill) %>">/kill</a>
```

`mix phoenix.server` を実行して `http://localhost:5000' に接続すると、Worker の状態の取得や inc/0 によるカウントが動くこと、また  `http://localhost:5000/kill` に接続すると Worker がクラッシュして再起動されることを確認できます。

非常に簡単ですね！