[Elixir] Supervisorのchild_spec

この記事は [Elixir その2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/elixir2) 19日目です。

前日は、「[[Elixir] GenServer.init関数で重い処理](https://qiita.com/mnishiguchi/private/c50fed098acd506d9559)」でした。

## はじめに

Elixirの[「Let it crash」](https://elixirforum.com/t/understanding-the-advantages-of-let-it-crash-term/9748)はAWESOMEです。プロセスが再起動されるのを見るたびに感動します。そのプロセス管理に重要になってくるのが[`Supervisor`](https://hexdocs.pm/elixir/Supervisor.html)ですが、`child_spec`がちょっとややこしくないですか？僕はそう思ったので、今後迷わないように構文として整理しました。サンプルコードは手作りです。

僕は[「Elixir in Action」by Sasa Juric](https://www.manning.com/books/elixir-in-action-second-edition)でElixirの基礎を学びました。色んなアイデアはそこにあります。

## child_spec

[ドキュメント](https://hexdocs.pm/elixir/Supervisor.html#module-child_spec-1)によると、`child_spec`は僕たちが書いたモジュールをどのように扱ってほしいのか（起動、停止、再起動など）を`Supervisor`に教えるためのものだそうです。ですので、プロセスを`Supervisor`に管理させたい場合は必ず`child_spec`が必要となることになります。

### データ構造

- [Map](https://hexdocs.pm/elixir/Map.html)
- [必須キー](https://hexdocs.pm/elixir/Supervisor.html#module-child-specification): `:id`, `:start`
- [オプションキー](https://hexdocs.pm/elixir/Supervisor.html#module-child-specification): `restart`, `:shutdown`, `:type`, `:modules`

## 色んな定義のやり方

`HelloWorker`というモジュールを`Supervisor`に管理させたい場合のサンプルです。

### 基本形1（Supervisor.start_linkに直書き）

引数の渡し方が若干ややこしく感じました。渡せる引数はひとつだけ。複数渡したい場合は、タプルやリスト等を使う。`Supervisor`に子プロセスを追加するときにエラーが出たとき、僕の場合、引数の渡し方が間違っている場合がほどんどです。

```elixir
defmodule HelloWorker do
  # 重要: ここでは意図的に何も`use`していない。

  def start_link(word: word) do
    GenServer.start_link(__MODULE__, word)
  end

  def init(word) do
    IO.puts("Started: #{word}")
    {:ok, word}
  end
end
```

```elixir
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
        [
          %{
            id: HelloWorker,
            start: {HelloWorker, :start_link, [[word: "Elixirたのしいですね〜"]]}
          }
        ],
        strategy: :one_for_one
      )
Started: Elixirたのしいですね〜
{:ok, #PID<0.528.0>}

# Supervisorが子プロセスを持っていることを確認。
iex> Supervisor.count_children sup
%{active: 1, specs: 1, supervisors: 0, workers: 1}

iex> Supervisor.which_children sup
[{HelloWorker, #PID<0.529.0>, :worker, [HelloWorker]}]

# 子プロセスを停止してみる。
iex> GenServer.stop(pid(0, 529, 0))
Started: Elixirたのしいですね〜
:ok

# PIDが新しくなっていることを確認。
iex> Supervisor.which_children sup
[{HelloWorker, #PID<0.533.0>, :worker, [HelloWorker]}]
```

### 基本形2（子モジュールにchild_spec関数を定義）

ドキュメントによると、こちらの構文が推奨されてます。子モジュールに変更があったときに`child_spec`を書き換えることを忘れるのを防ぐのが理由です。

```elixir
defmodule HelloWorker do
  # 重要: ここでは意図的に何も`use`していない。

  def child_spec(args) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [args]},
    }
  end

  def start_link(word: word) do
    GenServer.start_link(__MODULE__, word)
  end

  def init(word) do
    IO.puts("Started: #{word}")
    {:ok, word}
  end
end
```

```elixir
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
        [{HelloWorker, [word: "Elixirたのしいですね〜"]}],
        strategy: :one_for_one
      )
Started: Elixirたのしいですね〜
{:ok, #PID<0.183.0>}
```

### 省略形１

`use GenServer`や`use Task`をモジュールに含めると、それらがデフォルトの`child_spec`関数を挿入してくれるので、その関数の定義に従えば自分で`child_spec`関数を定義するは必要なし。それが気に入らない場合は、自分で定義する。

[GenServer -  How to supervise](https://hexdocs.pm/elixir/GenServer.html#module-how-to-supervise)
[Task - Supervised tasks](https://hexdocs.pm/elixir/Task.html#module-supervised-tasks)

```elixir
defmodule HelloWorker do
  use GenServer

  def start_link(word: word) do
    GenServer.start_link(__MODULE__, word)
  end

  def init(word) do
    IO.puts("Started: #{word}")
    {:ok, word}
  end
end
```

```elixir
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
        [
          # HelloWorker.child_spec(word: "Elixirたのしいですね〜")が呼ばれる。
          {HelloWorker, [word: "Elixirたのしいですね〜"]}
        ],
        strategy: :one_for_one
      )
Started: Elixirたのしいですね〜
{:ok, #PID<0.126.0>}
```

### 省略形2

子モジュールが引数を無視する場合にのみ使えるパターン。子モジュール名を渡すと、`Supervisor`は`HelloWorker`を`{HelloWorker, []}`であると解釈してくれる。

```elixir
defmodule HelloWorker do
  use GenServer

  def start_link(_args) do
    GenServer.start_link(__MODULE__, nil)
  end

  def init(_args) do
    word = "Elixirたのしいですね〜"
    IO.puts("Started: #{word}")
    {:ok, word}
  end
end
```

```elixir
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
        [
          # HelloWorker.child_spec([])が呼ばれる。
          HelloWorker
        ],
        strategy: :one_for_one
      )
Started: Elixirたのしいですね〜
{:ok, #PID<0.148.0>}
```

## 同じモジュールから複数のプロセスを起動したい場合

- `use GenServer`等が挿入してくれるデフォルトの`child_spec/1` は`__MODULE__`をIDとしているので、複数のプロセスを起動する場合は使用できない。
- `use GenServer`等を使用するとしても、プロセスごとに一意のIDを生成するように自分で`child_spec/1`を定義すること。

```elixir
defmodule HelloWorker do
  use GenServer

  def child_spec([word: word] = args) do
    %{
      id: {__MODULE__, word},
      start: {__MODULE__, :start_link, [args]},
    }
  end

  def start_link(word: word) do
    GenServer.start_link(__MODULE__, word)
  end

  def init(word) do
    IO.puts("Started: #{word}")
    {:ok, word}
  end
end
```

```elixir
# Supervisorを起動。
iex> {:ok, sup} = Supervisor.start_link(
        [
          # HelloWorker.child_spec(word: "Elixirたのしいですね〜")が呼ばれる。
          {HelloWorker, [word: "Elixirたのしいですね〜"]},
          # HelloWorker.child_spec(word: "ナウでヤングでcoolなNervesフレームワーク")が呼ばれる。
          {HelloWorker, [word: "ナウでヤングでcoolなNervesフレームワーク"]}
        ],
        strategy: :one_for_one
      )
Started: Elixirたのしいですね〜
Started: ナウでヤングでcoolなNervesフレームワーク
{:ok, #PID<0.156.0>}
```

## さいごに

因みにElixirと[ナウでヤングでcoolなNerves](https://www2.slideshare.net/takasehideki/elixiriotcoolnerves-236780506)を使っての今どきの電子工作は本当に楽しいので、オススメします。

今後更に新しいことを学んだら随時内容も更新していこうと思います。

明日は「[[Elixir] Referenceの作り方](https://qiita.com/mnishiguchi/items/4d982a16c2448790cad4)」です。引き続き、Elixirを楽しみましょう。

- [Elixir その1 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/elixir)
- [Elixir その2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/elixir2)
- [【毎日自動更新】QiitaのElixir LGTMランキング！](https://qiita.com/torifukukaiou/items/1edb3e961acf002478fd) by @torifukukaiou