search
LoginSignup
2
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

Elixir その2 Advent Calendar 2020 Day 19

posted at

updated at

[Elixir] Supervisorのchild_spec

この記事は Elixir その2 Advent Calendar 2020 19日目です。

前日は、「[Elixir] GenServer.init関数で重い処理」でした。

はじめに

Elixirの「Let it crash」はAWESOMEです。プロセスが再起動されるのを見るたびに感動します。そのプロセス管理に重要になってくるのがSupervisorですが、child_specがちょっとややこしくないですか?僕はそう思ったので、今後迷わないように構文として整理しました。サンプルコードは手作りです。

僕は「Elixir in Action」by Sasa JuricでElixirの基礎を学びました。色んなアイデアはそこにあります。

child_spec

ドキュメントによると、child_specは僕たちが書いたモジュールをどのように扱ってほしいのか(起動、停止、再起動など)をSupervisorに教えるためのものだそうです。ですので、プロセスをSupervisorに管理させたい場合は必ずchild_specが必要となることになります。

データ構造

色んな定義のやり方

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

基本形1(Supervisor.start_linkに直書き)

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

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
# 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を書き換えることを忘れるのを防ぐのが理由です。

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

省略形1

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

GenServer - How to supervise
Task - Supervised tasks

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
# 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

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

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
# 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を定義すること。
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
# 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: ナウでヤングでcoolNervesフレームワーク
{:ok, #PID<0.156.0>}

さいごに

因みにElixirとナウでヤングでcoolなNervesを使っての今どきの電子工作は本当に楽しいので、オススメします。

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

明日は「[Elixir] Referenceの作り方」です。引き続き、Elixirを楽しみましょう。

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
What you can do with signing up
2
Help us understand the problem. What are the problem?