Pull to refresh

Введение в gen_event: Уведомления об изменениях счета

Reading time8 min
Views4.1K
Original author: Mitchell Hashimoto
От переводчика: попытаюсь подхватить флаг, брошенный tiesto из-за недостатка кармы (кстати, огромное ему спасибо за первые переводы; именно благодаря им я узнал об авторе, чей цикл статей надеюсь продолжить переводить).

Ссылки на предыдущие части можно найти в конце статьи.


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

Сценарий: С появлением программного обеспечения для центрального сервера и банкоматов на местах, ErlyBank начал оптимистично воспринимать свою технологическую базу. Но в качестве средств защиты от одного из конкурентов они хотели бы реализовать систему, которая отсылает уведомления при снятии определенного количества наличиности. Они хотят иметь возможность изменять пороговое значение суммы вывода наличных, после которого срабатывает уведомление, без перезагрузки ПО. Руководство решило нанять нас, чтобы модернизировать текущую версию в соответствии с поставленной задачей.

Результат: Мы создадим систему уведомлений, основанную на событиях, используя gen_event. Это даст нам базовый фундамент для создания в будущем и других уведомлений, в то же время позволяя легко интегрироваться в текущее серверное ПО.

Что такое gen_event?


gen_event служит в Erlang/OTP модулем поведения (behavior; я буду использовать терминологию, которая может отличаться от переводов предыдущего автора; ранее в цикле термин переводился как «интерфейс», что понятнее для знакомых с ООП, но менее соответствует функциональной сути Erlang — прим. переводчика), реализующим функции обработки событий. Это реализуется запуском процесса менеджера событий (event manager) со множеством процессов обработчиков (handlers), запущенных вокруг него. Менеджер событий получает события (events) от других процессов, затем каждый обработчик по очереди получает получает уведомления о событиях и может делать с ними все, что угодно.

Ожидаемыми методами обратной связи (callbacks) для обработчика являются:
  • init/1 — Инициализирует обработчик.
  • handle_event/2 — Обрабатывает любые события, отправленные менеджеру событий, который прослушивается обработчиком.
  • handle_call/2 — Обрабатывает событие, которое отправлено как вызов (call) менеджеру событий. Вызов (call) в данном контексте — это то же самое, что и вызов (call) в поведении gen_server: выполнение приостанавливается до отправки ответа.
  • handle_info/2 — Обрабатывает любые сообщения, отправленные обработчику, не являющиеся событием (event) или вызовом (call).
  • terminate/2 — Вызывается, когда обработчик завершает работу, чтобы процесс мог освободить любые занятые им ресурсы.
  • code_change/3 — Вызывается, когда модуль подвергается замене кода непосредственно во время работы. Этот режим не описывается в данной статье, но будет героем одной из будущих статей этой серии.

По сравнению с двумя описанными ранее в этой серии модулями поведения текущий имеет относительно небольшое количество методов обратной связи. Однако gen_event может использоваться столь же часто и обладает не меньшими возможностями, чем другие модули OTP.

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


Первое, что нам нужно сделать — это создать менеджер событий. Это относительно простая задача, так как заключается просто в запуске сервера gen_event, и, присовокупляя несколько методов API поверх, легко добавить новые обработчики, отправить уведомления (notifications) и так далее.

Взгляните на мой каркас менеджера событий. Далее я буду давать фрагменты, используя его как основу.

Как вы можете видеть, в файле нет ничего необычного. Стартовый метод такой же, как у других модулей поведения OTP. Я включил основное API для добавления в менеджер событий обработчика:
%%--------------------------------------------------------------------
%% Function: add_handler(Module) -> ok | {'EXIT',Reason} | term()
%% Description: Adds an event handler
%%--------------------------------------------------------------------
add_handler(Module) ->
  gen_event:add_handler(?SERVER, Module, []).

Этот метод просто добавляет в менеджер событий обработчик события, расположенный в текущем Модуле. Я так же добавил и простой в использовании метод, необходимый, чтобы через менеджер событий отправить уведомление:
%%--------------------------------------------------------------------
%% Function: notify(Event) -> ok | {error, Reason}
%% Description: Sends the Event through the event manager.
%%--------------------------------------------------------------------
notify(Event) ->
  gen_event:notify(?SERVER, Event).

Это так же должно быть довольно легко понять. Метод просто отправляет событие менеджеру событий. gen_event:notify/2 — асинхронный запрос, так что возврат произойдет немедленно.

Подключение менеджера событий к серверу


Мы хотим быть уверены, что менеджер событий уже запущен до того, как стартует сервер, так что сейчас мы заставим его запуститься из серверного модуля явно. Позже, в другой статье, мы заставим Деревья контроля (supervisor trees) сделать это за нас. Вот мой новый код для инициализации сервера:

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


Теперь, когда мы предполагаем, что менеджер событий будет запущен в течение всего времени жизни серверного процесса, у нас есть возможность отправлять сообщения в определенные моменты. Наш клиент, ErlyBank, хочет знать, когда происходит снятие (у автора написано deposit, т.е. внесение, но это явно ошибка, и дальнейший код это подтверждает — прим. переводчика) определенной суммы. Вот как я подключил это к серверу:

handle_call({withdraw, Name, Amount}, _From, State) ->
  case dict:find(Name, State) of
    {ok, {_PIN, Value}} when Value < Amount ->
      {reply, {error, not_enough_funds}, State};
    {ok, {PIN, Value}} ->
      NewBalance = Value - Amount,
      NewState = dict:store(Name, {PIN, NewBalance}, State),
      % Send notification
      eb_event_manager:notify({withdraw, Name, Amount, NewBalance}),
      {reply, {ok, NewBalance}, NewState};
    error ->
      {reply, {error, account_does_not_exist}, State}
  end;


Теперь во время снятия с помощью менеджера событий так же генерируется событие. Помните, что метод уведомления — асинхронный, а менеджер событий и обработчики выполняются в отдельных процессах. Это означает, что уведомление пройдет параллельно и не замедлит сделку. Конечно, если процессор перегружен, выполнение каждого процесса займет больше времени, но теоретически это должно произойти одновременно.

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

Все содержимое eb_server.erl можно увидеть здесь.

Каркас обработчика


У меня есть основной каркас для всех модулей OTP, с которого я всегда начинаю. Таковой для обработчика события находится здесь.

Одна вещь, которая отличает этот модуль: здесь нет метода start_link или start. Так случилось потому, что для подключения обработчика события мы воспользуется методом eb_event_manager:add_handler(Module), который, в действительности, стартует и порождает для нас процесс!

init([]) ->
  {ok, 500}.


Метод инициализации для обработчика gen_event похож на таковые во всех остальных модулях поведения Erlang/OTP тем, что он возвращает {ok, State}, где State представляет собой состояние данных для процесса. И данном случае мы возвращаем число 500, которое будем использовать для указания порога предупреждения при уведомлениях о снятии со счета.

Обработка уведомления о снятии наличных


Единственная цель этого обработчика событий — принять уведомление о снятии и сделать что-нибудь, если сумма снятия превышает определенный порог. Событие отправлено с помощью асинхронного сообщения gen_event:notify/2. Асинхронные уведомления обработчикам обслуживаются методом handle_event:

handle_event({withdraw, Name, Amount, NewBalance}, State) when Amount >= State ->
  io:format("WITHDRAWAL NOTIFICATION: ~p withdrew ~p leaving ~p left.~n", [Name, Amount, NewBalance]),
  {ok, State};
handle_event(_Event, State) ->
  {ok, State}.


Обработать сообщение просто. Мы добавили шаблон для проверки на сообщение о снятии и защиту, чтобы получить событие только в случае превышения суммы снятия порогового значения.

Когда сумма снятия превышает пороговое значение, мы выводим ее на терминал.

Готовый модуль eb_withdrawal_handler.erl можно взять здесь.

Изменение порога во время работы


ErlyBank таже отметил, что они хотят иметь возможность менять порог уведомления о снятии наличных во время работы. Для этого мы добавим метод API для фактического обработчика. Вот он:

%%--------------------------------------------------------------------
%% Function: change_threshold(Amount) -> {ok, Old, NewThreshold}
%% | {error, Reason}
%% Description: Changes the withdrawal amount threshold during runtime
%%--------------------------------------------------------------------
change_threshold(Amount) ->
  gen_event:call(eb_event_manager, ?MODULE, {change_threshold, Amount}).


Это вводит новый метод get_event, метод вызова (call). Этот метод отправляет запрос к конкретному обработчику и ожидает ответ, и, следовательно, является синхронным. Его аргументы: call(EventManager, Handler, Message). Таким образом в нашем случае в качестве первого параметра мы укажем модуль менеджера событий, который зарегистрирован с текущем процессе. В качестве второго параметра укажем имя модуля обработчика, и отправим сообщение для изменения порогового значения.

Обработаем этот запрос в методе обратной связи handle_call/2:

handle_call({change_threshold, Amount}, State) ->
  io:format("NOTICE: Changing withdrawal threshold from ~p to ~p~n", [State, Amount]),
  {ok, {ok, State, Amount}, Amount};
handle_call(_Request, State) ->
  Reply = ok,
  {ok, Reply, State}.


Сначала выведем на терминал сообщение о том, что порог меняется, затем ответим сообщением {ok, OldThreshold, NewThreshold} и установим новое пороговое значение как состояние данных (state). При получения следующего уведомления о снятии обработчик будет использовать новое значение предупреждения! :)

Полностью модуль eb_withdrawal_handler можно посмотреть здесь.

Заключение


В этой статье о gen_event я рассказал о написании менеджера событий, отправке событий, реализации обработчика событий, получении этих событий и вызове обработчика. Единственное, что я не раскрыл — это часть gen_event, позволяющая обработчику событий отключиться или заменить себя на другой обработчик. Причина заключается в том, что у меня нет собственного большого опыта работы с этим механизмом в реальной среде. Пока я не нашел применения этим возможностям. Но если вы хотите узнать о них — загляните в документацию.

И на этом завершаем третью часть цикла введения в Erlang/OTP. Статья четыре запланирована к публикации в течение нескольких дней и будет говорить о контроллерах (supervisors).



UPD.
Благодаря внимательности хабраюзера kutu обнаружено серьезное упущение, которое было допущено в оригинальном тексте и перекочевало затем в перевод. Обратите внимание: в статье нигде не указано место, откуда должен выполняться вызов eb_event_manager:add_handler(Module) для нашего обработчика. Вызов и не осуществляется, и, следовательно, наш обработчик не включается в работу. Логичным местом для подключения обработчика на данном этапе будет init(Args) в eb_serverl.erl. Вот так должен выглядеть этот метод после внесения нужных изменений:

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


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

Кроме того, в перевод добавлена ссылка на полную версию обработчика eb_withdrawal_handler, которую я случайно забыл привести.


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

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

Автор перевода — tiesto:
2. Введение в gen_fsm: Банкомат ErlyBank
1. Введение в gen_server: «ErlyBank»
0. Введение в Open Telecom Platform/Открытую Телекомуникационную Платформу(OTP/ОТП)
-1. Предыстория
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+31
Comments4

Articles