Это четвертая статья в серии «Введение в ОТП». Если вы только что присоединились к нам, рекомендую начать с первой части, в которой говорится о 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. Его задача — вернуть список дочерних процессов и стратегии перезапуска для каждого процесса, чтобы контролёр знал, за чем следить и что предпринять, если что-то пойдет не так.
Одна из вещей, которые я реализовал в предыдущей статье о gen_events, заключалась в явном запуске процесса менеджера событий в методе инициализации модуля eb_server. Тогда это было единственной возможностью, которая у меня была, если я действительно хотел с легкостью запустить сервер с такой зависимостью. Но теперь, раз мы собираемся реализовать запуск и остановку, используя контролёр, у нас есть возможность запустить менеджер событий в дереве контроля. Так что давайте-ка уберем запуск eb_event_manager из кода сервера.
Чтобы сделать это, просто уберите строку 84, которая запускает менеджер событий, из модуля eb_server. Так же я добавил на это место вызов add_handler, чтобы подключить к менеджеру событий обработчик eb_withdrawal_handler (если вы последовательно читаете и реализуете описанное в настоящем цикле переводов, включая дополнение к предыдущей статье, то вам ничего добавлять не нужно, потому что мы уже сделали это ранее; после исключения запуска менеджера событий ваш код метода инициализации должен выглядеть так же, как нижеследующий — прим. переводчика). Теперь метод init модуля eb_server должен выглядеть подобно этому:
Кликните сюда, чтобы увидеть eb_server.erl после внесенных изменений.
Основной каркас для написания контролера можно увидеть здесь. Как вы могли заметить, в нем присутствует метод запуска и базовый метод инициализации, который на данный момент возвращает стратегию перезапуска и несуществующие спеки потомка. Стратегии перезапуска (restart stategies) и спецификации потомков (child specifications) раскрываются в следующих разделах этой статьи.
Сохраните каркас как eb_sup.erl. Именование этого файла — еще одно соглашение. Контролёр определенной группы всегда имеет суффикс "_sup." Это не обязательное требование, но является стандартной практикой.
Контролёр имеет единую стратегию перезапуска, которую он использует в сочетании со ��пецификациями потомков, чтобы определить действия в случае, если один из его потомков умрет. Нижеследующий список содержит возможные стратегии перезапуска:
Стратегия перезапуска указывается в следующем формате:
Его очень просто понять, проговорив по-русски: Если потомок перезапускается чаще, чем MaxRetries раз за MaxTime секунд, то контролёр завершает все дочерние процессы и затем прекращает работу сам. Это сделано для того, чтобы предотвратить бесконечную петлю перезапусков потомка.
Метод init контролёра отвечает за возврат списка спецификаций потомков. Эти спецификации рассказывают контролёру, какие процессы запускать и как это сделать. Контролёр запускает процессы в порядке «слева направо» (от начала списка к его концу). Стратегия перезапуска — это кортеж со следующим форматом:
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, так как его влияние на модули динамическое (различные обработчики, которые не могут быть определены сразу). Этот список используется только для управления релизами и не важен в контексте данной статьи, но будет использоваться в одной из будущих статей, посвященной управлению релизами.
Ух! Так много информации для усвоения за столь короткое время. У меня ушло довольно много времени на то, чтобы запомнить формат спецификаций потомков и различные стратегии перезапуска, так что не переживайте, если не можете сделать этого сразу. Вы всегда можете освежить информации в руководстве по контролёрам.
Первое, что требуется запустить — это менеджер событий, потому что сервер от него зависит. Спецификация потомка выглядит как-то так:
После чтения раздела о синтаксисе спецификации потомков этот кусок кода должен быть достаточно простым. Возможно, вам понадобится вернуться назад и свериться с описанием, чтобы понять действие каждого параметра, и это абсолютно нормально! Лучше задержаться и разобраться в коде, чем покивать головой и забыть все через пару минут. Я полагаю, что одной «странной» штукой в описании спецификации будет указание списка модулей как «dynamic» (динамический; в данном случае это атом — прим. переводчика). Это сделано потому, что речь идет о gen_event, и список модулей, которые в нем используются, динамический, т.к. к нему подключаются обработчики (число которых может меняться в процессе работы — прим. переводчика). В прочих случаях вы должны перечислить все модули, которые использует процесс.
Вот метод инициализации после включения в него спеки потомков:
Я предпочитаю каждую спецификацию потомка присвоить переменной, и затем использовать содержимое этих переменные для возврата, нежели поместить спеки непосредственно в возвращаемое значение. Одна из крупнейших мозолей Erlang проявляется в момент, когда программист встраивает списки и кортежи так глубоко, что вы не можете увидеть, где заканчивается один и начинается другой, поэтому рекомендую и вам присвоить каждый переменной.
Если вы сейчас скомпилируете и запустите контролёр (я думаю, стоит это сделать!), то после запуска метода start_link, принадлежащего контролёру, введите whereis(eb_event_manager), и команда должна вернуть идентификатор (pid) процесса менеджера событий. Затем, если вы убьете контролёр, выполнив exit(whereis(eb_sup), kill), и потом попытаетесь снова получить идентификатор eb_event_manager, то в ответ должны получить сообщение о том, что он не определен, так как процесс был убит.
Так же, для прикола, убейте eb_event_manager во время работы под управлением контролёра. Подождите несколько секунд и проверьте процесс. Он должен восстановиться!
Со справкой по спецификации потомков и примером, приведенным выше, вы должны знать достаточно для того, чтобы запустить сервер и банком. Так что если вы чувствуете эту задачу заманчивой, сделайте это сейчас. Если же нет, то я привел спецификации и для того, и для другого ниже:
После создания этих спецификаций добавьте их в список, возвращаемый инициализационным методом. Убедитесь, что поставили их после менеджера событий.
Вы можете увидеть завершенный 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. Предыстория
Сценарий: Момент, который нам нравится в банках и банкоматах, заключается в том, что они всегда на том же месте. Используя банкомат, мы можем снять или положить деньги когда захотим, 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. Предыстория
