Это четвертая статья в серии «Введение в ОТП». Если вы только что присоединились к нам, рекомендую начать с первой части, в которой говорится о gen_server и закладывается фундамент нашей банковской системы. Если же вы способный ученик, можете взглянуть на готовые к настоящему моменту модули: eb_server.erl, eb_event_manager.erl, eb_withdrawal_handler.erl и eb_atm.erl.

Сценарий: Момент, который нам нравится в банках и банкоматах, заключается в том, что они всегда на том же месте. Используя банкомат, мы можем снять или положить деньги когда захотим, 24 часа в сутки. Или пойти в любое отделение банка, когда оно открыто, зная, что будем иметь полный доступ к нашим финансам. Чтобы гарантировать это, необходимо быть уверенным в том, что наша система автоматизации ErlyBank всегда остается рабочей: процессы должны быть запущены постоянно. ErlyBank поручил нам реализовать эту цель. Стопроцентный uptime! (Или настолько близкий, насколько мы сможем обеспечить)

Результат: Используя контролёр (supervisor) OTP, мы создадим процесс, чья обязанность — следить за запущенными процессами и удостовериться, что они активны.

Что такое контролёр


Контролёр — это процесс, который отслеживает то, что называется дочерними процессами. Если дочерний процесс «сдулся», контролёр использует ст��атегию перезапуска (restart strategy) этого потомка для перезапуска. Этот способ может обеспечить вечную работоспособность систем Erlang.

Контролёр — часть того, что называется деревом контроля (supervision tree). Хорошо написанное приложение Erlang/OTP запускается, начиная с корневого контролёра, который следит за дочерними контролёрами, которые, в свою очередь, отслеживают дополнительные контролёры или процессы. Идея в том, что если контролёр вылетает, контролёр-родитель его перезапускает, и так далее вверх до корневого контролёра. В среде исполнения Erlang присутствует отличный режим, в котором вся система отслеживается и рестартует, если умирает корневой контролёр. Таким образом, дерево контроля всегда будет работоспособно.

У контролёра есть только один метод обратной связи: init/1. Его задача — вернуть список дочерних процессов и стратегии перезапуска для каждого процесса, чтобы контролёр знал, за чем следить и что предпринять, если что-то пойдет не так.

Разделяем eb_server и менеджер событий


Одна из вещей, которые я реализовал в предыдущей статье о gen_events, заключалась в явном запуске процесса менеджера событий в методе инициализации модуля eb_server. Тогда это было единственной возможностью, которая у меня была, если я действительно хотел с легкостью запустить сервер с такой зависимостью. Но теперь, раз мы собираемся реализовать запуск и остановку, используя контролёр, у нас есть возможность запустить менеджер событий в дереве контроля. Так что давайте-ка уберем запуск eb_event_manager из кода сервера.

Чтобы сделать это, просто уберите строку 84, которая запускает менеджер событий, из модуля eb_server. Так же я добавил на это место вызов add_handler, чтобы подключить к менеджеру событий обработчик eb_withdrawal_handler (если вы последовательно читаете и реализуете описанное в настоящем цикле переводов, включая дополнение к предыдущей статье, то вам ничего добавлять не нужно, потому что мы уже сделали это ранее; после исключения запуска менеджера событий ваш код метода инициализации должен выглядеть так же, как нижеследующий — прим. переводчика). Теперь метод init модуля eb_server должен выглядеть подобно этому:

init([]) ->
  eb_event_manager:add_handler(eb_withdrawal_handler),
  {ok, dict:new()}.

Кликните сюда, чтобы увидеть eb_server.erl после внесенных изменений.

Каркас контролёра


Основной каркас для написания контролера можно увидеть здесь. Как вы могли заметить, в нем присутствует метод запуска и базовый метод инициализации, который на данный момент возвращает стратегию перезапуска и несуществующие спеки потомка. Стратегии перезапуска (restart stategies) и спецификации потомков (child specifications) раскрываются в следующих разделах этой статьи.

Сохраните каркас как eb_sup.erl. Именование этого файла — еще одно соглашение. Контролёр определенной группы всегда имеет суффикс "_sup." Это не обязательное требование, но является стандартной практикой.

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


Контролёр имеет единую стратегию перезапуска, которую он использует в сочетании со ��пецификациями потомков, чтобы определить действия в случае, если один из его потомков умрет. Нижеследующий список содержит возможные стратегии перезапуска:

  • one_for_one — Когда один из дочерних процессов умирает, контролёр перезапускает его. Другие потомки не трогаются.
  • one_for_all — Когда один из дочерних процессов умирает, все другие потомки останавливаются, а затем все перезапускаются.
  • rest_for_one — Когда один из дочерних процессов умирает, «остаток» потомков, определенных в списке спецификаций потомков после умершего, завершается, после чего они все стартуют заново.

Стратегия перезапуска указывается в следующем формате:

{RestartStrategy, MaxRetries, MaxTime}

Его очень просто понять, проговорив по-русски: Если потомок перезапускается чаще, чем MaxRetries раз за MaxTime секунд, то контролёр завершает все дочерние процессы и затем прекращает работу сам. Это сделано для того, чтобы предотвратить бесконечную петлю перезапусков потомка.

Синтаксис и основные понятия спецификации потомков


Метод init контролёра отвечает за возврат списка спецификаций потомков. Эти спецификации рассказывают контролёру, какие процессы запускать и как это сделать. Контролёр запускает процессы в порядке «слева направо» (от начала списка к его концу). Стратегия перезапуска — это кортеж со следующим форматом:

{Id, StartFunc, Restart, Shutdown, Type, Modules}

Определения:
Id = term()
 StartFunc = {M,F,A}
  M = F = atom()
  A = [term()]
 Restart = permanent | transient | temporary
 Shutdown = brutal_kill | int()>=0 | infinity
 Type = worker | supervisor
 Modules = [Module] | dynamic
  Module = atom()


Id используется только внутри контролёра для хранения спецификации потомков, но общее соглашение подразумевает использование в качестве ID имени модуля, за исключением случаев, когда вы запускаете несколько экземпляров модуля; в последнем случае добавьте к ID номер.

StartFunc — это кортеж в формате {Module, Function, Args}, который указывает функцию, вызов которой запускает процесс. ОЧЕНЬ ВАЖНО: функция запуска обязана запустить процесс и привязать (link) к нему и должна вернуть {ok, Pid}, {ok, Pid, Other} или {error, Reason}. Обычные методы OTP start_link следуют этому правилу. Но если вы реализуете модуль, который запускает свои собственные процессы, убедитесь, что используете для их запуска spawn_link.

Restart — один из трех атомов (atom), описанных в блоке кода выше. Если в качестве restart используется атом "permanent", то процесс всегда запускается заново. Если значение — "temporary", то процесс никогда заново не запускается. И если это значение равно "transient", то процесс запускается заново только в случае непредвиденного завершения.

Shutdown объясняет контролёру, как завершать дочерние процессы. Атом "brutal_kull" завершает потомка без вызова его метода завершения. Любое целое число выше нуля подразумевает таймаут для корректного завершения. Атом "infinity" вежливо завершит процесс и будет ждать его остановки вечно.

Type говорит контролеру, что из себя представляет потомок: другой контролёр или любой прочий процесс. Если это контролёр, используйте атом «supervisor», иначе воспользуйтесь атомом «worker».

Modules — это либо список модулей, на которые этот процесс влияет, либо атом «dynamic». В 95% случаев в списке для этого значения вы будете использовать единственный модуль обратной связи OTP. «Dynamic» используется в случае, если процесс — gen_event, так как его влияние на модули динамическое (различные обработчики, которые не могут быть определены сразу). Этот список используется только для управления релизами и не важен в контексте данной статьи, но будет использоваться в одной из будущих статей, посвященной управлению релизами.

Ух! Так много информации для усвоения за столь короткое время. У меня ушло довольно много времени на то, чтобы запомнить формат спецификаций потомков и различные стратегии перезапуска, так что не переживайте, если не можете сделать этого сразу. Вы всегда можете освежить информации в руководстве по контролёрам.

Спека потомка менеджера событий


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

EventManager = {eb_event_manager,{eb_event_manager, start_link,[]},
            permanent,2000,worker,dynamic}.

После чтения раздела о синтаксисе спецификации потомков этот кусок кода должен быть достаточно простым. Возможно, вам понадобится вернуться назад и свериться с описанием, чтобы понять действие каждого параметра, и это абсолютно нормально! Лучше задержаться и разобраться в коде, чем покивать головой и забыть все через пару минут. Я полагаю, что одной «странной» штукой в описании спецификации будет указание списка модулей как «dynamic» (динамический; в данном случае это атом — прим. переводчика). Это сделано потому, что речь идет о gen_event, и список модулей, которые в нем используются, динамический, т.к. к нему подключаются обработчики (число которых может меняться в процессе работы — прим. переводчика). В прочих случаях вы должны перечислить все модули, которые использует процесс.

Вот метод инициализации после включения в него спеки потомков:

init([]) ->
  EventManager = {eb_event_manager,{eb_event_manager, start_link,[]},
              permanent,2000,worker,dynamic},
  {ok,{{one_for_one,5,10}, [EventManager]}}.

Я предпочитаю каждую спецификацию потомка присвоить переменной, и затем использовать содержимое этих переменные для возврата, нежели поместить спеки непосредственно в возвращаемое значение. Одна из крупнейших мозолей Erlang проявляется в момент, когда программист встраивает списки и кортежи так глубоко, что вы не можете увидеть, где заканчивается один и начинается другой, поэтому рекомендую и вам присвоить каждый переменной.

Если вы сейчас скомпилируете и запустите контролёр (я думаю, стоит это сделать!), то после запуска метода start_link, принадлежащего контролёру, введите whereis(eb_event_manager), и команда должна вернуть идентификатор (pid) процесса менеджера событий. Затем, если вы убьете контролёр, выполнив exit(whereis(eb_sup), kill), и потом попытаетесь снова получить идентификатор eb_event_manager, то в ответ должны получить сообщение о том, что он не определен, так как процесс был убит.

Так же, для прикола, убейте eb_event_manager во время работы под управлением контролёра. Подождите несколько секунд и проверьте процесс. Он должен восстановиться!

Сервер и банкомат


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

Server = {eb_server, {eb_server, start_link, []},
              permanent,2000,worker,[eb_server]},
  ATM = {eb_atm, {eb_atm, start_link, []},
         permanent,2000,worker,[eb_atm]},


После создания этих спецификаций добавьте их в список, возвращаемый инициализационным методом. Убедитесь, что поставили их после менеджера событий.

Вы можете увидеть завершенный eb_sup.erl, нажав здесь.

Добавление и удаление потомков во время выполнения


К несчастью, я не смог придумать остроумный сценарий, позволяющий вставить этот механизм в ErlyBank, но чувствовал, как важно отметить возможность динамически добавлять и удалять спецификации потомков в уже запущенный процесс контролёра, используя методы start_child и delete_child.

Они довольно просты, так что я не буду повторять здесь руководство, на которое сослался; вы можете перейти непосредственно к нему и ознакомиться с этими методами.

Заключение


В этой статье о контроллерах я ввёл такие понятия, как дерево контроля, стратегии перезапуска, спецификации потомков и динамическое добавление и удаление потомков.

На этом заканчивается четвертая статья из цикла «Введение в Erlang/OTP». Пятая статья уже готова, запланирована к публикации в ближайшие несколько дней и представит приложения (applications).



Статьи из серии

4. Использование контролёров для того, чтобы удержать ErlyBank на плаву (текущая статья)
3. Введение в gen_event: Уведомления об изменениях счета

Автор перевода — tiesto:
2. Введение в gen_fsm: Банкомат ErlyBank
1. Введение в gen_server: «ErlyBank»
0. Введение в Open Telecom Platform/Открытую Телекомуникационную Платформу(OTP/ОТП)
-1. Предыстория