From the translator: I will try to pick up the flag thrown by tiesto due to lack of karma (by the way, thank you so much for the first translations; it was thanks to them that I learned about the author, whose cycle of articles I hope to continue translating).
Links to the previous parts can be found at the end of the article.This is the third article in the
series “Introduction to OTP” . I recommend
starting with the first part , which talks about gen_server and lays the foundation of our banking system, if you have not done it yet. On the other hand, if you are a capable student, you can take a look at the ready-made modules
eb_atm.erl and
eb_server.erl .
')
Scenario : With the advent of software for the central server and ATMs in the field, ErlyBank began to be optimistic about its technological base. But as a means of protection from one of the competitors, they would like to implement a system that sends notifications when a certain amount of cash is removed. They want to be able to change the threshold value of the cash withdrawal, after which the notification is triggered, without rebooting the software. The management decided to hire us to upgrade the current version in accordance with the task.
Result : We will create an event-based notification system using
gen_event . This will give us a basic foundation for creating future notifications and at the same time allowing us to easily integrate into the current server software.
What is gen_event?
gen_event serves as a behavior module in Erlang / OTP (
behavior; I will use terminology that may differ from the previous author’s translations; earlier in the cycle the term was translated as “interface”, which is more understandable for those familiar with OOP, but less consistent with the functional nature of Erlang - approx. translator ) that implements event handling functions. This is
accomplished by running the
event manager process with a variety of handlers running around it. The event manager receives events (
events ) from other processes, then each handler in turn receives receives notifications about events and can do whatever they want with them.
The expected
callbacks for the handler are:
- init / 1 - Initializes the handler.
- handle_event / 2 - handles any events sent by the event manager that the handler listens on.
- handle_call / 2 - Handles an event that is sent as a call to the event manager. A call (call) in this context is the same as a call (call) in gen_server behavior: execution is suspended until the response is sent.
- handle_info / 2 - Handles any messages sent to the handler that are not an event or a call.
- terminate / 2 - Called when the handler exits, so that the process can release any resources it occupies.
- code_change / 3 - Called when a module is subjected to a code change directly during operation. This mode is not described in this article, but will be the hero of one of the future articles in this series.
Compared to the two behavior modules described earlier in this series, the current behavior has a relatively small number of feedback methods. However, gen_event can be used as often and has the same capabilities as other OTP modules.
Creating an event manager
The first thing we need to do is create an event manager. This is a relatively simple task, as it is simply to start the gen_event server, and, adding several API methods on top, it is easy to add new handlers, send notifications, and so on.
Take a look at
my event manager frame . Further I will give fragments, using it as a basis.
As you can see, there is nothing unusual in the file. The starting method is the same as other OTP behavior modules. I enabled the main API to add a handler to the event manager:
%%--------------------------------------------------------------------
%% Function: add_handler(Module) -> ok | {'EXIT',Reason} | term()
%% Description: Adds an event handler
%%--------------------------------------------------------------------
add_handler(Module) ->
gen_event:add_handler(?SERVER, Module, []).
This method simply adds an event handler located in the current
Module to the event manager. I also added an easy-to-use method needed to send a notification through the event manager:
%%--------------------------------------------------------------------
%% Function: notify(Event) -> ok | {error, Reason}
%% Description: Sends the Event through the event manager.
%%--------------------------------------------------------------------
notify(Event) ->
gen_event:notify(?SERVER, Event).
It should also be pretty easy to understand. The method simply sends the event to the event manager.
gen_event: notify / 2 is an asynchronous request, so a return will occur immediately.
Connecting the event manager to the server
We want to be sure that the event manager is already running before the server starts, so now we will force it to start from the server module explicitly. Later, in another article, we will make the
Supervisor trees do it for us. Here is my new code to initialize the server:
init([]) ->
eb_event_manager:start_link(),
{ok, dict:new()}.
Now, when we assume that the event manager will be running during the entire lifetime of the server process, we have the ability to send messages at certain times. Our client, ErlyBank, wants to know when a withdrawal takes place (
a deposit is written for the author, that is, a deposit, but this is clearly a mistake, and a further code confirms this with a comment of the translator) of a certain amount. Here is how I connected it to the server:
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;
Now during the withdrawal via the event manager, an event is also generated. Remember that the notification method is asynchronous, and the event manager and handlers are executed in separate processes. This means that the notification will take place in parallel and will not slow down the transaction. Of course, if the processor is overloaded, the execution of each process will take longer, but theoretically it should happen at the same time.
Also note that I do not check here whether the withdrawal amount has exceeded a certain amount. ErlyBank did not specify what amount they wanted to use as a threshold, and it would be rather foolish to program it hard in the server process. In general, it is a good idea to leave all the logic in the handlers and just call the notification. And this is exactly what we will do next!
All the contents of eb_server.erl can be seen
here .
Handler frame
I have a basic framework for all the OTP modules with which I always start. The one for the event handler
is here .
One thing that distinguishes this module: there is no
start_link or
start method. This happened because to connect the event handler, we will use the
eb_event_manager: add_handler (Module) method, which, in fact, starts and spawns the process for us!
init([]) ->
{ok, 500}.
The initialization method for the gen_event handler is similar to those in all other Erlang / OTP behaviors in that it returns
{ok, State} , where
State is the data state for the process. And in this case, we return the number 500, which we will use to specify the warning threshold for notifications of withdrawal from the account.
Processing cash withdrawal notice
The sole purpose of this event handler is to accept the withdrawal notification and do something if the withdrawal amount exceeds a certain threshold. The event was sent using an asynchronous message
gen_event: notify / 2 . Asynchronous notifications to handlers are serviced by the method
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}.
To process the message is simple. We have added a template for checking on the withdrawal message and protection in order to receive the event only if the withdrawal amount exceeds the threshold value.
When the withdrawal amount exceeds the threshold value, we output it to the terminal.
Ready module eb_withdrawal_handler.erl can
be found here .
Threshold change during operation
ErlyBank also noted that they want to be able to change the cash withdrawal notification threshold during operation. To do this, we will add an API method for the actual handler. Here he is:
%%--------------------------------------------------------------------
%% 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}).
This introduces a new get_event method, a call method. This method sends a request to a specific handler and waits for a response, and, therefore, is synchronous. Its arguments are:
call (EventManager, Handler, Message) . Thus, in our case, we will specify the event manager module as the first parameter, which is registered with the current process. As the second parameter, we specify the name of the handler module, and send a message to change the threshold value.
We process this request in the feedback method
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}.
First, we will display on the terminal a message that the threshold is changing, then we will respond with the message {ok, OldThreshold, NewThreshold} and set the new threshold value as the data state. Upon receipt of the next notice of withdrawal, the handler will use the new warning value! :)
Fully module eb_withdrawal_handler can be
viewed here .
Conclusion
In this article on
gen_event, I talked about writing an event manager, sending events, implementing an event handler, receiving these events, and invoking a handler. The only thing I didn’t disclose is the part of gen_event, which allows the event handler to disconnect or replace itself with another handler. The reason is that I do not have much experience of working with this mechanism in a real environment. While I have not found the use of these features. But if you want to know about them - look in the
documentation .
And this concludes the third part of the introduction cycle to Erlang / OTP. Article four is scheduled for publication in a few days and will be talking about controllers (
supervisors ).
UPD .
Due to the care of the klau
habrauser , a serious omission was revealed, which was made in the original text and later moved into translation. Please note that the article does not indicate anywhere where the
eb_event_manager: add_handler (Module) call should be made for our handler. The call and is not carried out, and, therefore, our handler is not included in the work. The logical place to connect the handler at this stage will be init (Args) in eb_serverl.erl. This is how this method should look like after making the necessary changes:
init([]) ->
eb_event_manager:start_link(),
eb_event_manager:add_handler(eb_withdrawal_handler),
{ok, dict:new()}.
In further articles, the author of the original text promises to ensure the launch of all start-up scripts from one place, but this will happen only in the following articles. Stay with us!
In addition, the translation added a
link to the full version of the eb_withdrawal_handler handler, which I accidentally forgot to bring.
Articles from the series
4.
Use of controllers to keep ErlyBank afloat3.
Introduction to gen_event: Account Change Notifications (current article)Translation by
tiesto :
2.
Introduction to gen_fsm: ErlyBank ATM1.
Introduction to gen_server: “ErlyBank”0.
Introduction to the Open Telecom Platform / Open Telecommunication Platform (OTP / OTP)-one.
Prehistory