Do you want to pick up from where you left of?
Take me there

OTP Супервизоры

Супервизоры это специальные процессы с единственной целью: мониторинг других процессов. Супервизоры позволяют нам создавать отказоустойчивые приложения при помощи автоматического перезапуска дочерних процессов в случае их отказа.

Настройка

Магия супервизоров заключена в функции Supervisor.start_link/2. Помимо запуска нашего супервизора и его потомков, она позволяет нам определять стратегию, которую будет использовать наш супервизор для управления дочерними процессами.

Давайте начнём, используя SimpleQueue из урока OTP Concurrency:

Создадим новый проект используяmix new simple_queue --sup для генерации проекта с деревом супервизора. Код для модуля SimpleQueue должен находиться в lib/simple_queue.ex, а код супервизора, который мы добавим, пойдет в lib/simple_queue/application.ex.

Дочерние процессы определяются с помощью списка двумя способами. Либо списком названий модулей:

defmodule SimpleQueue.Application do
  use Application

  def start(_type, _args) do
    children = [
      SimpleQueue
    ]

    opts = [strategy: :one_for_one, name: SimpleQueue.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Либо списком кортежей, если вы хотите добавить параметры конфигурации:

defmodule SimpleQueue.Application do
  use Application

  def start(_type, _args) do
    children = [
      {SimpleQueue, [1, 2, 3]}
    ]

    opts = [strategy: :one_for_one, name: SimpleQueue.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Если мы выполним iex -S mix, то увидим, что SimpleQueue автоматически запустился:

iex> SimpleQueue.queue
[1, 2, 3]

Если наш SimpleQueue процесс упадёт или завершится, супервизор автоматически перезапустит его как ни в чём не бывало.

Стратегии перезапуска

На данный момент для Супервизора доступны три стратегии перезапуска:

Спецификация дочерних процессов

После запуска супервизор должен знать, как запускать/останавливать/перезапускать свои дочерние процессы. Каждый модуль дочернего процесса должен иметь функцию child_spec/1 для определения этих поведений. Макросы use GenServer, use Supervisor, и use Agent автоматически определяют этот метод для нас (SimpleQueue имеет use GenServer, поэтому нам не надо вносить правки в этот модуль), но если вам необходимо определить его вручную, child_spec/1 должен вернуть ассоциативный массив параметров:

def child_spec(opts) do
  %{
    id: SimpleQueue,
    start: {__MODULE__, :start_link, [opts]},
    shutdown: 5_000,
    restart: :permanent,
    type: :worker
  }
end

Если процесс имеет тип :worker, время по умолчанию будет равно 5000.

По умолчанию для всех процессов

Динамичный Супервизор

Супервизор обычно запускаются вместе со списком дочерних процессов во время запуска приложения. Однако, иногда дочерние процессы остаются неизвестны во время запуска нашего приложения (Например, мы можем иметь веб-приложение, которое запускает новые процессы для обработки подключений пользователей к нашему сайту). В этих случаях нам нужен супервизор, где дочерние процессы могут быть запущены по требованию. Решением является Динамичный Супервизор (DynamicSupervisor).

Поскольку мы не будем указывать дочерние процессы, нам нужно только определить параметры времени выполнения для супервизора. Динамичный Супервизор поддерживает только стратегию :one_for_one:

options = [
  name: SimpleQueue.Supervisor,
  strategy: :one_for_one
]

DynamicSupervisor.start_link(options)

Затем, чтобы запустить новый SimpleQueue динамически, мы используем start_child/2, которая берет на вход супервизор и модуль дочернего процесса с определенной спецификацией (повторюсь, т.к. SimpleQueue используют use GenServer, то спецификация дочернего процесса уже определена):

{:ok, pid} = DynamicSupervisor.start_child(SimpleQueue.Supervisor, SimpleQueue)

Наблюдатель задач

Для задач есть специальный супервизор — Task.Supervisor. Он разработан для динамически создаваемых задач и под капотом использует DynamicSupervisor.

Настройка

Включение Task.Supervisor ничем не отличается от других супервизоров:

children = [
  {Task.Supervisor, name: ExampleApp.TaskSupervisor, restart: :transient}
]

{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)

Основное различие между Supervisor и Task.Supervisor в параметре перезапуска по умолчанию - :temporary (задачи никогда не будут перезапущены).

Наблюдаемые задачи

При запущенном супервизоре мы можем использовать функцию start_child/2 для создания наблюдаемой задачи:

{:ok, pid} = Task.Supervisor.start_child(ExampleApp.TaskSupervisor, fn -> background_work end)

Если наша задача преждевременно выйдет из строя, она будет перезапущена. Это может быть особенно полезно при работе со входящими соединениями и обработке фоновых задач.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!