Бесплатный курс по Elixir. Зарегистрируйтесь для отслеживания прогресса →

Elixir: Супервизоры

Супервизоры - процессы которые следят за жизненным циклом других процессов (потомков). Благодаря супервизорам появляется возможность выстраивать внутри приложения специальную иерархическую структуру, называемую деревом супервизии. Дерево супервизии позволяет выстраивать отказоустойчивы приложения, логика обработки и перезапуска процессов в котором скрыта от пользователя приложения.

Супервизоры, как и дерево супервизии, являются частью OTP Erlang (Open Telecom Platform) - набором библиотек и подходов для упрощения работы с процессами. Далее по модулю мы познакомимся еще с одной важной сущностью OTP, помимо дерева супервизии и супервизоров.

Для запуска супервизора нужно передать ему список потомков с их спецификацией через функцию start_link. Либо реализовать модуль с функциями обратного вызова, соответствующих поведению Supervisor. Теперь создадим агента-счетчик, который передадим под наблюдение:

defmodule Counter do
  use Agent

  def start_link(initial_state \\ 0) do
    Agent.start_link(fn -> initial_state end, name: __MODULE__)
  end

  def current_value do
    Agent.get(__MODULE__, fn state -> state end)
  end

  def inc do
    Agent.update(__MODULE__, fn state -> state + 1 end)
  end

  def dec do
    Agent.update(__MODULE__, fn state -> state - 1 end)
  end

  def boom do
    raise "boom"
  end
end

При использовании поведения Agent, в модуль добавилась функция child_spec/1. Она и нужна для запуска агента через супервизора. У сгенерированной функции арность 1, для того, чтобы передать начальное состояние при инициализации агента. Вызовем эту функцию:

Counter.child_spec(0)
# => %{id: Counter, start: {Counter, :start_link, [0]}}

Counter.child_spec(5)
# => %{id: Counter, start: {Counter, :start_link, [5]}}

Как видно из примеров, child_spec/1 всего лишь возвращает словарь с конфигурацией процесса, идентификатором процесса выступает его модуль, под ключом :start находится информация о том, как запускать процесс. Перевести эту спецификацию можно следующим образом: в первом вызове child_spec/1 процесс нужно запустить Counter.start_link(0), во втором случае Counter.start_link(5).

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

children = [
  {Counter, 0}
]

{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_all)
pid
# => #PID<0.127.0>

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

children = [Counter]

{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_all)
# => #PID<0.129.0>

Однако в таком случае, в start_link/1 агента передастся пустой список:

defmodule Counter do
  use Agent

  def start_link(initial_state \\ 0) do
    IO.inspect(initial_state)
    Agent.start_link(fn -> initial_state end, name: __MODULE__)
  end
end

children = [Counter]

{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_all)
# => []
# => #PID<0.131.0>

В таких случаях нужно переписать инициализацию процесса таким образом:

def start_link(opts) do
  {initial_value, opts} = Keyword.pop(opts, :initial_value, 0)
  Agent.start_link(fn -> initial_value end, opts)
end

Теперь, когда процесс запущен, можно получить служебную информацию о супервизоре с помощью count_children и which_children, а так же пользоваться агентом по прямому назначению:

{:ok, pid} = Supervisor.start_link([{Counter, 0}], strategy: :one_for_all)

Supervisor.count_children(pid)
# => %{active: 1, workers: 1, supervisors: 0, specs: 1}

Counter.inc()
Counter.inc()
Counter.current_value()
# => 2

Supervisor.which_children(pid)
# => [{Counter, #PID<0.146.0>, :worker, [Counter]}]

Рассмотрим запуск супервизора Supervisor.start_link(children, strategy: :one_for_all), а конкретно strategy: :one_for_all. Эта опция является стратегией перезапуска процессов которые завершились аварийно. Таких стратегий перезапуска несколько:
- :one_for_one - если потомок-процесс аварийно завершился, то только этот процесс и будет перезапущен;
- :one_for_all - если один из потомков аварийно завершился, то все дочерние процессы супервизора завершатся и перезапустятся;
- :rest_for_one - если один из потомков аварийно завершился, то только этот процесс и все следующие за ним потомки завершатся и перезапустятся.

Зачастую используются стратегии :one_for_one и :rest_for_one.

Помимо стратегии перезапуска, в супервизоре можно указать, каким образом завершать наблюдаемые процессы. А точнее, какой сигнал выхода отправить процессу при, например, перезапуске. Можно указать приоритет у процессов потомков, например, если есть какой-то важный процесс потомок, без которого супервизия не имеет смысла и после завершения работы этого потомка супервизор получает сигнал от этого процесса и супервизор завершает работу (process shutdown). Об этом можно подробнее почитать в официальной документации. Тепрерь посмотрим на примере, как работает стратегия :one_for_all:

{:ok, supervisor_pid} = Supervisor.start_link([{Counter, 0}], strategy: :one_for_one)

Counter.inc()
Counter.current_value()
# => 1

# теперь вызовем функцию которая бросит исключение
Counter.boom()
# => ** (RuntimeError) boom
# =>   iex:71: Counter.boom/0
# =>   iex:67: (file)

Supervisor.count_children(supervisor_pid)
# => %{active: 1, workers: 1, supervisors: 0, specs: 1}

Counter.current_value()
# => 0

Процесс Counter завершился с ошибкой, супервизор перехватил исключение и перезапустил процесс счетчик с начальным состоянием по умолчанию.

Задание

Супервизоры могут задаваться через модули, для этого нужно в модуле использовать поведение Supervisor и определить функцию обратного вызова init/1, которая будет вызвана при запуске супервизора через start_link. В файле с решением описано два агента, ваша задача дописать модуль Solution, добавив две функции:
- start_link - функция для запуска супервизора из модуля
- init - функция, которая запустится перед стартом супервизора, в котором указывается список потомков, стратегия перезапуска и т.д.

В функции init сделайте потомками Decrementor и Incrementor, а в стратегии перезапуска укажите :one_for_one.

{:ok, _} = Solution.start_link()

Supervisor.which_children(Solution)
# => [
# =>   {Decrementor, #PID<0.196.0>, :worker, [Decrementor]},
# =>   {Incrementor, #PID<0.195.0>, :worker, [Incrementor]}
# => ]

Supervisor.count_children(Solution)
# => %{active: 2, workers: 2, supervisors: 0, specs: 2}

Incrementor.run()
Incrementor.current_value()
# => 1

Decrementor.current_value()
# => 0
Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

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

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

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

Полезное


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе