An introduction to gen_event: Account Notifications

Posted by on September 10, 2008

This is the third article in the otp introduction series. If you haven’t yet, I recommend you start with the first article which talks about gen_server and lays the foundation for our bank system. Again, if you’re a quick learner, you can look at the completed eb_atm.erl and eb_server.erl here.

The Scenario: With central server and ATM software in place, ErlyBank is started to feel good about their technological foundation. But as a security measure, one of their competitors, they’d like a system implemented which dispatches notifications when a withdrawal over a certain amount is executed. They want the ability to change the withdrawal notification amount threshold during runtime. They’ve chosen to hire us to streamline this into the current system software.

The Result: We’ll create an event-based notification system using gen_event. This will give us a base foundation for creating more notifications in the future, while allowing us to easily plug-in to the current server software.

What is gen_event?

gen_event is an Erlang/OTP behavior module for implementing event handling functionality. This works by running an event manager process, with many handler processes running along-side it. The event manager receives events from other processes, and each handler in turn is notified about the events, and can do what it pleases with them.

The callbacks expected for a gen_event handler module are:

  • init/1 - Initializes the handler.
  • handle_event/2 - Handles any events sent to the notification manager it is listening to.
  • handle_call/2 - Handles an event which is sent as a call to the notification manager. A call in this context is the same as a gen_server call: it blocks until a response is sent.
  • handle_info/2 - Handles any non-event and non-call messages sent to the handler.
  • terminate/2 - Called when the handler is quitting so the process can clean up any open resources.
  • code_change/3 - Called when the module is experiencing a real-time system upgrade. This will not be covered in this article, but it will play a central role of a future article in this series.

Compared to the previous two behavior modules covered in this series, there are relatively few callback methods. But gen_event has just as much use and just as much power as the other OTP modules.

Creating the Event Manager

First thing we need to do is create the event manager. This is a relatively simple task, since its really just starting a gen_event server and adding a few API methods on top easily add new handlers, send notifications, etc.

View my event manager skeleton here. I will be pasting in snippets from this point on.

As you can see, there is nothing out of the ordinary in the file. The start-up method is the same as every other OTP behavior module. And I’ve included a basic 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 just adds an event handler located at Module to the event manager. And I’ve also added an easy to use notify method to send along 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).

 

This also should be pretty easy to understand. It just sends the event along to the event manager. gen_event:notify/2 is an asynchronous request, it will return immediately.

Hooking the Event Manager Into the Server

We want to make sure that the event manager is always up before the server starts, so for now we will be explicitly putting the start code into the server module. Later, in another article, we will use supervisor trees to do this for us. Here is my new init code for the server:

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

 

Now that we can assume the event manager will be up during the life of the server process, we can dispatch events at certain times. Our client, ErlyBank, wants to know when a deposit over a certain amount occurs. Here is how I hooked this into 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 whenever a withdrawal occurs, the event is raised through the event manager too. Remember that the notify method is asynchronous and the event manager and handlers all run on separate processes. This makes all this notification happen concurrently, therefore it won’t slow down the withdrawal transaction. Of course, if the cpu is being slammed, it may take more time to execute each process, but in theory, they should happen at the same time.

Also notice that I don’t check here whether the withdrawal is over a certain amount. ErlyBank didn’t clarify with me what amount they wanted as the threshold, and it would be rather silly to hardcode it into the server process. Its generally a good idea to keep all the logic in the handlers, and just raise the notification. And this is what we will do next!

The entire contents of eb_server.erl can be viewed here.

The Handler Skeleton

As with all OTP modules, I have a basic skeleton I always start with. The one for event handlers can be viewed here.

One thing that is different about this module is that there is no start_link or start method. This is because to add an event handler we will be using the eb_event_manager:add_handler(Module) method, which actually starts and spawns the process for us!

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

 

The init method for a gen_event handler is similar to all other Erlang/OTP behavior modules in that it returns {ok, State} where State represents the state data for the process. In this case we’ve returned 500, which we will use to signify what the warning threshold for withdrawal notifications is.

Handling the Withdrawal Notification

The sole purpose of this event handler is to process the withdrawal notification and do something if the amount withdrawn is over a certain threshold. The event is sent with gen_event:notify/2 which is an asynchronous message. Asynchronous notifications to handlers are handled in the handle_event method.

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}.

 

Handling the message is simple. We add a matcher to match the withdrawal message, and we add a guard when Amount >= State to only get events when the amount withdrawn is above the threshold.

When the amount is above the threshold, we output it to the terminal.

The complete eb_withdrawal_handler.erl can be viewed here.

Changing the Threshold During Runtime

ErlyBank also mentioned that they want the ability to change the withdrawal notification amount threshold during runtime. To do this, we will add an API method to the actual handler. Here is the API method:

%%--------------------------------------------------------------------
%% 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 gen_event method, the call method. This method sends a request to a specific handler and expects a response, and therefore is synchronous. The arguments are: call(EventManager, Handler, Message). So for our arguments we put the event manager module, which that process is registered as, as the first parameter. We put the handler module as the second parameter, and we send a message to change the threshold.

We handle this request in a callback 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}.

 

We first output to the terminal that the threshold is changing, and then we reply with {ok, OldThreshold, NewThreshold} and set the new state data to the new threshold. Upon receiving the next withdrawal notification, the handler will begin using the new threshold! :)

The complete eb_withdrawal_handler can be viewed here.

Final Notes

In this article about gen_event I introduced writing an event manager, dispatch events, writing an event handler, processing those events, and calling an event handler. The only thing I didn’t really cover which is part of gen_event is the ability for an event handler to remove itself or swap itself with another handler. The reason for this is because I don’t have much experience with these myself in a production environment. I haven’t found a position yet where I’ve needed to use them. But if you wish to learn about them, check out the gen_event manual page.

And that concludes part three of the Erlang/OTP introduction series. Article four is queued up for publishing in a few days and will cover supervisors.

Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. zamous Sep 10, 2008 08:01

    Wow, you are so prolific. This is great stuff. You should write a book on OTP. Can’t wait for supervisors.

  2. Michael Greene Sep 10, 2008 10:30

    Thanks for these articles, they are really filling a huge void.

  3. Matt Sep 10, 2008 12:07

    Where does eb_event_manager:add_handler get called?

  4. Mitchell Sep 10, 2008 12:19

    It doesn’t! Good catch! I was just testing this in the shell and always did it manually.

    I suppose a logical place to put this, at this point in the code, would be in eb_server after it initializes the the event manager. :-\

    But, in a future article, I will decouple most of these components and have a general startup script which will do this. :)

  5. Alain O'Dea Sep 14, 2008 17:58

    Would it make sense to use gen_event to capture a log of transactions for ErlyBank with something like mnesia? My first impression is that it would work really nicely.

  6. Mitchell Sep 14, 2008 18:10

    Alain,

    This would definitely be a great use for gen_event. :) The basic premise for gen_event is to send everything that could potentially be loggable to it, and let the handlers handle the logic and figuring out what to do with all this data.

    Mitchell

  7. Ricardo Sep 28, 2008 03:20

    Thanks Mitchell for this great series.

    I’ve noticed that the skeleton does not include “-behaviour(gen_event).” as the other previos ones. Is that correct?

    Also, a great topic for future articles could be the interaction between an Erlang program with another application via sockets. Why? many times there are some applications that can be controlled via its own API using sockets but fail when the number of open connections is too high. I think that an Erlang broker could be put in the middle; open one connection to the application and receive the request from the application to forward them to the app; and to react to the application responses.

  8. Ricardo Sep 30, 2008 11:29

    Matt,

    Indeed, in my case, the handler did never work until I called it explicitly in the server init:

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

  9. Arek Nov 04, 2008 02:21

    Mitchell,
    Thank you for this effort, you have unique style in conveying your idea…
    I wish you write a book about OTP in the same writing style

  10. jimmyrr Jul 10, 2009 15:30

    I think there’s a bug in your skeleton file - the handle_call function should return a tuple with a first value of ‘reply’ - and not ‘ok’. Btw - thanks - awesome blog.

Comments

Comments: