Как работают супервизоры в Elixir
В Erlang и Elixir супервизоры — это процессы, управляющие дочерними процессами и перезапускающие их в случае возникновения ошибок. В этой статье подробно рассматривается реализация супервизоров в Elixir.
Но прежде чем перейти к самим супервизорам, неплохо было бы как следует ознакомиться с модулями gen_server
и supervisor
. Можно обойтись и без этого, если вам уже доводилось работать с эквивалентными модулями Elixir, так как они просто передают вызовы модулям Erlang, не меняя своего поведения.
Начнём, пожалуй, с примера из самой документации Elixir:
В приведённом примере метод start_link
создаёт супервизор, а метод init
реализует обратный вызов, используемый поведением из Supervisor
. Рассмотрим Supervisor
подробнее.
Пробежимся по поведениям. Инструкция use Supervisor
разворачивается во время компиляции в поведение, указанное в макросе __using__
. Посмотрим на код макроса __using__
из модуля Supervisor
На момент написания статьи макрос __using__
выглядел так:
С помощью @behaviour
в супервизоре проверяется наличие необходимых функций обратного вызова, а оператор import
подгружает в MyApp.Supervisor
дополнительные функции из модуля Supervisor.Spec
. Именно в нём определены функции worker
и supervise
.
Если в двух словах, то инструкция use Supervisor
добавляет в проект несколько новых функций и следит за наличием нужных функций обратного вызова.
Работа супервизора начинается с вызова MyApp.Supervisor.start_link
. Он делегируется функции Supervisor.start_link
, передавая ссылку на себя (с помощью макроса __MODULE__
).
Посмотрим на реализацию Supervisor.start_link
:
Обратите внимание, что модуль Elixir делегирует вызовы модулю Erlang :supervisor
. Но это не повод для паники! Доверьтесь мне и загляните
Введя в поиске start_link
, вы наткнётесь на инструкцию export
, позволяющую использовать функцию за пределами модуля, спецификацию, описывающую доступные для функции типы данных, и, собственно, саму её реализацию:
Видите? Уже что-то знакомое. Просто запускаем gen_server
, не вдаваясь gen_server
, который ожидает увидеть реализацию нескольких функций обратного вызова.
Гораздо больший интерес представляет метод init
. Обратите внимание, что хоть MyApp.Supervisor
и содержит реализацию функции init
, но это не функция обратного вызова, которая будет вызвана далее. Если вернуться к реализации функции start_link
в модуле :supervisor
из Erlang, можно заметить, что в неё передаётся параметр self
. Это означает, что :supervisor.init
— та самая искомая функция.
Посмотрите на её исходный код:
Во-первых, вызывается функция Mod:init(Args)
, которая в свою очередь вызывает функцию MyApp.Supervisor.init
. Ещё раз взглянем на код:
Не стоит забывать, что worker
и supervise
— хелперы из Supervisor.Spec
. Не будем ходить вокруг да около, а перейдём к самому главному, то есть рассмотрим подробнее, как всё это реализуется в Erlang. worker
возвращает child_spec
, а supervise
— такой кортеж: {:ok, { {strategy, max_retries, max_seconds}, child_specs} }
.
Во-вторых, :supervisor.init
вызывает process_flag
, после чего происходит перехват process_flag
перехватывает сигнал и вместо этого посылает процессу сообщение {’EXIT’, from_pid, reason}
. Далее будет показано, каким образом процесс-супервизор использует значение from_pid
для выявления уничтоженного процесса и его перезапуска.
Итак, мы рассмотрели функцию :supervisor.init
и узнали про перехват сигналов выхода. Теперь перейдём к инициализации состояния и дочерних процессов. Так как в данном примере супервизоры :simple_one_for_one
не используются, то обратим внимание лишь на инициализацию дочерних процессов init_children
.
Небольшое отступление: State#state.name
обращается к переменной State
как к записи state
и «вытаскивает» из неё поле name
. {:state, «josh», [1, 2, 3]}
(подобно типу enum
в других языках). Записи — это лишь способ отвязать позицию поля от его значения. Так выглядит исходный код записи state
, прописанный в начале:
Можно видеть, что запись state
содержит поле name, а выражение SupName = State#state.name
просто обращается к кортежу State
как к записи state
, вытаскивая и сохраняя в SupName
поле, связанное с name
.
Что касается функции check_startspec
, то она проверяет данные и помещает полученные от MyApp.Supervisor.init
спецификации в запись (
Суть функции :supervisor.init_children
заключается в вызове функции start_children
:
Наблюдается небольшая рекурсия: берётся каждый дочерний процесс и вызывается функция do_start_child
:
apply
в Erlang — метод динамического вызова функций (подобно send
в Ruby или apply
/call
в Javascript). Он динамически обращается к функции обратного вызова, определённой в Supervisor.Spec.worker
: worker
. И, наконец, он вызывает функцию report_progress
и передаёт информационное событие о запуске супервизором нового процесса в менеджер событий Erlang
Полдела сделано. Идём дальше.
Резюмируя вышеизложенное: мы рассмотрели, как запускаются супервизоры, перехватываются сигналы выхода и создаются дочерние процессы. Но как же перезапускаются дочерние процессы? Помните, как process_flag
отлавливает сигналы выхода, превращая их в кортежи {’EXIT’, from_pid, reason}
? А то, что супервизоры построены на основе библиотеки gen_server
? gen_server
обрабатывает все сообщения, кроме типов call
/cast
с помощью функции handle_info
(подробнее о ней
Отсюда видно, как он создаёт сообщение, содержащее идентификатор дочернего процесса (pid
) и причину его завершения, и передаёт эти данные в метод restart_child
! Победа! Функции do_start_child
имеет много общего с ранее рассмотренной функцией restart_child
, поэтому оставим вопрос её реализации на самостоятельное изучение. Если вам интересно узнать, как супервизоры реализуют стратегии своего завершения, взгляните
Подведём итоги. Elixir использует общепринятую модель построения супервизоров, которая базируется на интерфейсе модуля :supervisor
из Erlang, построенного на библиотеке :gen_server
. Настройки из Elixir передаются в модуль :supervisor
, который на их основе запускает дочерние процессы. Супервизор управляет дочерними процессами, перехватывая их сигналы выхода, и превращает эти сигналы в сообщения. В нём реализована функция обратного вызова handle_info
, принимающая сообщения о выходе и перезапускающая необходимый воркер. И последнее, что стоит упомянуть, — отчёты выводятся в менеджер событий Erlang :error_logger
.