Как работают супервизоры в Elixir
В Erlang и Elixir супервизоры — это процессы, управляющие дочерними процессами и перезапускающие их в случае возникновения ошибок. В этой статье подробно рассматривается реализация супервизоров в Elixir.
Но прежде чем перейти к самим супервизорам, неплохо было бы как следует ознакомиться с модулями gen_server и supervisor. Можно обойтись и без этого, если вам уже доводилось работать с эквивалентными модулями Elixir, так как они просто передают вызовы модулям Erlang, не меняя своего поведения.
Начнём, пожалуй, с примера из самой документации Elixir:
defmodule MyApp.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, [])
end
def init([]) do
children = [
worker(Stack, [[:hello]])
]
# supervise/2 is imported from Supervisor.Spec
supervise(children, strategy: :one_for_one)
end
endВ приведённом примере метод start_link создаёт супервизор, а метод init реализует обратный вызов, используемый поведением из Supervisor. Рассмотрим Supervisor подробнее.
Пробежимся по поведениям. Инструкция use Supervisor разворачивается во время компиляции в поведение, указанное в макросе __using__. Посмотрим на код макроса __using__ из модуля Supervisor .
На момент написания статьи макрос __using__ выглядел так:
defmacro __using__(_) do
quote location: :keep do
@behaviour Supervisor
import Supervisor.Spec
end
endС помощью @behaviour в супервизоре проверяется наличие необходимых функций обратного вызова, а оператор import подгружает в MyApp.Supervisor дополнительные функции из модуля Supervisor.Spec. Именно в нём определены функции worker и supervise.
Если в двух словах, то инструкция use Supervisor добавляет в проект несколько новых функций и следит за наличием нужных функций обратного вызова.
Работа супервизора начинается с вызова MyApp.Supervisor.start_link. Он делегируется функции Supervisor.start_link, передавая ссылку на себя (с помощью макроса __MODULE__).
Посмотрим на реализацию Supervisor.start_link:
def start_link(module, arg, options \\ []) when is_list(options) do
case Keyword.get(options, :name) do
nil ->
:supervisor.start_link(module, arg)
atom when is_atom(atom) ->
:supervisor.start_link({:local, atom}, module, arg)
{:global, _term} = tuple ->
:supervisor.start_link(tuple, module, arg)
{:via, via_module, _term} = tuple when is_atom(via_module) ->
:supervisor.start_link(tuple, module, arg)
other ->
raise ArgumentError, """
expected :name option to be one of:
* nil
* atom
* {:global, term}
* {:via, module, term}
Got: #{inspect(other)}
"""
end
endОбратите внимание, что модуль Elixir делегирует вызовы модулю Erlang :supervisor. Но это не повод для паники! Доверьтесь мне и загляните .
Введя в поиске start_link, вы наткнётесь на инструкцию export, позволяющую использовать функцию за пределами модуля, спецификацию, описывающую доступные для функции типы данных, и, собственно, саму её реализацию:
start_link(Mod, Args) ->
gen_server:start_link(supervisor, {self, Mod, Args}, []).Видите? Уже что-то знакомое. Просто запускаем gen_server, не вдаваясь . Главное, что здесь нужно усвоить, — это то, что супервизоры построены на основе модуля gen_server, который ожидает увидеть реализацию нескольких функций обратного вызова.
Гораздо больший интерес представляет метод init. Обратите внимание, что хоть MyApp.Supervisor и содержит реализацию функции init, но это не функция обратного вызова, которая будет вызвана далее. Если вернуться к реализации функции start_link в модуле :supervisor из Erlang, можно заметить, что в неё передаётся параметр self. Это означает, что :supervisor.init — та самая искомая функция.
Посмотрите на её исходный код:
init({SupName, Mod, Args}) ->
process_flag(trap_exit, true),
case Mod:init(Args) of
{ok, {SupFlags, StartSpec}} ->
case init_state(SupName, SupFlags, Mod, Args) of
{ok, State} when ?is_simple(State) ->
init_dynamic(State, StartSpec);
{ok, State} ->
init_children(State, StartSpec);
Error ->
{stop, {supervisor_data, Error}}
end;
ignore ->
ignore;
Error ->
{stop, {bad_return, {Mod, init, Error}}}
end.Во-первых, вызывается функция Mod:init(Args), которая в свою очередь вызывает функцию MyApp.Supervisor.init. Ещё раз взглянем на код:
def init([]) do
children = [
worker(Stack, [[:hello]])
]
# supervise/2 is imported from Supervisor.Spec
supervise(children, strategy: :one_for_one)
endНе стоит забывать, что 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.
init_children(State, StartSpec) ->
SupName = State#state.name,
case check_startspec(StartSpec) of
{ok, Children} ->
case start_children(Children, SupName) of
{ok, NChildren} ->
{ok, State#state{children = NChildren}};
{error, NChildren, Reason} ->
_ = terminate_children(NChildren, SupName),
{stop, {shutdown, Reason}}
end;
Error ->
{stop, {start_spec, Error}}
end.Небольшое отступление: State#state.name обращается к переменной State как к записи state и «вытаскивает» из неё поле name. представляют собой более или менее структурированный тип данных, так как они хранятся в виде кортежей типа {:state, «josh», [1, 2, 3]} (подобно типу enum в других языках). Записи — это лишь способ отвязать позицию поля от его значения. Так выглядит исходный код записи state, прописанный в начале:
-record(state, {name,
strategy :: strategy() | 'undefined',
children = [] :: [child_rec()],
dynamics :: {'dict', ?DICT(pid(), list())}
| {'set', ?SET(pid())}
| 'undefined',
intensity :: non_neg_integer() | 'undefined',
period :: pos_integer() | 'undefined',
restarts = [],
dynamic_restarts = 0 :: non_neg_integer(),
module,
args}).Можно видеть, что запись state содержит поле name, а выражение SupName = State#state.name просто обращается к кортежу State как к записи state, вытаскивая и сохраняя в SupName поле, связанное с name.
Что касается функции check_startspec, то она проверяет данные и помещает полученные от MyApp.Supervisor.init спецификации в запись ().
Суть функции :supervisor.init_children заключается в вызове функции start_children:
start_children(Children, SupName) -> start_children(Children, [], SupName).
start_children([Child|Chs], NChildren, SupName) ->
case do_start_child(SupName, Child) of
{ok, undefined} when Child#child.restart_type =:= temporary ->
start_children(Chs, NChildren, SupName);
{ok, Pid} ->
start_children(Chs, [Child#child{pid = Pid}|NChildren], SupName);
{ok, Pid, _Extra} ->
start_children(Chs, [Child#child{pid = Pid}|NChildren], SupName);
{error, Reason} ->
report_error(start_error, Reason, Child, SupName),
{error, lists:reverse(Chs) ++ [Child | NChildren],
{failed_to_start_child,Child#child.name,Reason}}
end;
start_children([], NChildren, _SupName) ->
{ok, NChildren}.Наблюдается небольшая рекурсия: берётся каждый дочерний процесс и вызывается функция do_start_child:
do_start_child(SupName, Child) ->
#child{mfargs = {M, F, Args}} = Child,
case catch apply(M, F, Args) of
{ok, Pid} when is_pid(Pid) ->
NChild = Child#child{pid = Pid},
report_progress(NChild, SupName),
{ok, Pid};
{ok, Pid, Extra} when is_pid(Pid) ->
NChild = Child#child{pid = Pid},
report_progress(NChild, SupName),
{ok, Pid, Extra};
ignore ->
{ok, undefined};
{error, What} -> {error, What};
What -> {error, What}
end.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 (подробнее о ней ). Именно так супервизор обрабатывает сигналы выхода из дочерних процессов.
handle_info({'EXIT', Pid, Reason}, State) ->
case restart_child(Pid, Reason, State) of
{ok, State1} ->
{noreply, State1};
{shutdown, State1} ->
{stop, shutdown, State1}
end;Отсюда видно, как он создаёт сообщение, содержащее идентификатор дочернего процесса (pid) и причину его завершения, и передаёт эти данные в метод restart_child! Победа! Функции do_start_child имеет много общего с ранее рассмотренной функцией restart_child, поэтому оставим вопрос её реализации на самостоятельное изучение. Если вам интересно узнать, как супервизоры реализуют стратегии своего завершения, взгляните , которая делегирует вызовы функции обратного вызова .
Подведём итоги. Elixir использует общепринятую модель построения супервизоров, которая базируется на интерфейсе модуля :supervisor из Erlang, построенного на библиотеке :gen_server. Настройки из Elixir передаются в модуль :supervisor, который на их основе запускает дочерние процессы. Супервизор управляет дочерними процессами, перехватывая их сигналы выхода, и превращает эти сигналы в сообщения. В нём реализована функция обратного вызова handle_info, принимающая сообщения о выходе и перезапускающая необходимый воркер. И последнее, что стоит упомянуть, — отчёты выводятся в менеджер событий Erlang :error_logger.