A simple gen_event example.

September 14, 2016

The gen_event behavior makes it simple to add basic event sourcing to your Erlang application.

Young couple walking through a closed, colorful, carnival. class=
© 2016 Tim Gouw for Unsplash

In OTP, an event manager is a named object to which events can be sent. An event can be, for example, an error, an alarm, or some information that is to be logged.

In the event manager, zero, one, or many event handlers are installed. When the event manager is notified about an event, the event is processed by all the installed event handlers.

There are four things you need to do to use a OTP event manager:

  1. write an event handler,
  2. start and name the event manager,
  3. add a handler to the manager,
  4. and notify the manager of an event

1. Write an event handler

This is counter module is an event handler that counts events and prints status updates to stdout.


    -module(counter).
    
    -behaviour(gen_event).
    
    -export([code_change/3, handle_call/2, handle_event/2,
             handle_info/2, init/1, terminate/2]).
    
    init(_Args) ->
        io:put_chars("** 0: inititalize to zero\n"), {ok, 0}.
    
    handle_event(Event, Count) ->
        io:format("** +1: got event ~p~n", [Event]),
        {ok, Count + 1}.
    
    handle_call(Request, Count) ->
        io:format("** got request ~p~n", [Request]),
        {ok, Count, Count}.
    
    code_change(_OldVsn, State, _Extra) -> {ok, State}.
    
    handle_info(_Info, State) -> {noreply, State}.
    
    terminate(_Args, _State) -> ok.
    

In this case, the state is simply the Count of events that have been received. This could be as complex as you like.

(Note that the state is passed to the code_change event, so if you release a new version of the handler and need to change the state shape, you can transform the old state to the new as part of the upgrade.)

2. Start and name the event manager

We name the event manager “event_dispatcher”.


        {ok, _Pid} = gen_event:start_link({local, event_dispatcher}),
    

3. Add a handler to the manager

After this, events are sent to the counter’s handle_event/2 method.


        gen_event:add_handler(event_dispatcher, counter, []),
    

4. Notify the manager of an event

An Event can be any Erlang term.


        Event = {an, event, {can, [be, any, erlang]}, term},
        ok = gen_event:notify(event_dispatcher, Event),
    

Getting at the state in the handler

With one additional step, the gen_event behavior makes it trivial to create a CQRS read modelWhat is CQRS? in your application.

You need to get the data out of the event handler!


        Request = {so, can, the, call, request},
        Count = gen_event:call(event_dispatcher, counter, Request),
    

The gen_event:call/3 gives you synchronous access to the read model state.

Putting it all together

Using the counter model exactly as shown above, we can write an escript to show all of these steps.


    $ cat run.escript 
    #! /usr/bin/env escript
    
    main(_Argv) ->
        {ok, _Pid} = gen_event:start_link({local, event_dispatcher}),
        gen_event:add_handler(event_dispatcher, counter, []),
        Event = {an, event, {can, [be, any, erlang]}, term},
        ok = gen_event:notify(event_dispatcher, Event),
        Request = {so, can, the, call, request},
        Count = gen_event:call(event_dispatcher, counter, Request),
        io:format("Event count = ~w~n", [Count]).
    
    $ cat Makefile 
    .PHONY: run
    run: counter.beam
    	./run.escript
    
    %.beam: %.erl
    	erlc $?
    
    $ make
    ./run.escript
    ** 0: inititalize to zero
    ** +1: got event {an,event,{can,[be,any,erlang]},term}
    ** got request {so,can,the,call,request}
    Event count = 1
    
    $ 
    

A major caveat …

Unlike the gen_server,“Gen_server is usually spawned as a separate process, and all user callbacks (such as Mod:handle_call/3, Mod:handle_cast/2, etc) are executed in the context of that process.” Gen Event Behavior Demystified, by Serge Aleynikov (retrieved Sep. 14, 2016) the event manager executes all it’s handlers in the process context of the event manager. The event manager protects itself against handler code that crashes by silently removing the handler.

If you are using a handler as a read model as I suggest above, silent removal will break your application. So, either you need to write a bullet-proof handler or you need to come up with an approach that if a read model crashes, it does not miss any events. But that’s a topic for another blog.

Some resources

  • Gen Event Behavior Demystified—already mentioned above, this wiki page is a well written and concise and describes one approach to supervising the handlers.
  • What is wrong with gen_event?—another good, if less positive, writeup on gen_event, discussing issues of state and supervision. Mentions that since gen_event:notify/2 is asynchronous (and thus provides no backpressure), in a massive and extended event storm the process queue can grow so large it leads to a crash.
  • Supervised event handlers in Erlang—see the pattern?

Change Log

Nov. 24, 2016

  • Correct photographers last name.
  • Add photo location as Coney Island.

Tags: erlang