Richard Kallos

In Defense of gen_event

:: erlang

gen_event has drawn ire from Erlang and Elixir developers for a while. Many posts have been written, and more than one conference talk has been given on various alternatives to gen_event. In this post, I make the case that despite its warts and age, gen_event is still a very useful module to know. I also make the case that its core design is ultimately a lot more flexible than many people give it credit for.

Certain design decisions give rise to flexible systems. For example, Erlang’s asynchronous sending of messages and synchronous receiving of messages with timeouts offer developers the full range of message passing behaviour. For synchronous sending, send your message, ideally with a unique reference, then do a receive, pattern matching on the reference you sent. This is basically what goes on when you use gen_*:call. For asynchronous receives, you can use a receive block with after 0 to instantly time out if no messages can be matched against. Here is some code:

  sync_send(Pid, Msg) ->
      Ref = make_ref(),
      Pid ! {Ref, Msg},
          {Ref, Reply} -> Reply

  async_recv() ->
          M -> {ok, M}
          0 -> none

Question: If Erlang gave you synchronous sends and asynchronous receives, how would you go about implementing asynchronous sends and synchronous receives?

The core design of Erlang’s message passing is flexible enough to express the message passing semantics that it didn’t implement by default. Please keep this in mind as we turn our attention to gen_event.

For those of you who are unfamiliar with gen_event, it works more-or-less like this: gen_event:start_link starts what’s called an event manager process. When you write a module that implements the gen_event behaviour, you are writing what’s called an event handler. An event manager has a list of installed event handlers, each with their own state. when a process calls gen_event:notify, the event manager will invoke its installed handlers one at a time (after all, the event manager is just one process). I’ll repeat for emphasis; an event manager process executes all of its installed event handlers one at a time. This decision draws some ire, particularly from José Valim, who has a blog post on how to replace gen_event with a supervisor (as the event manager) and a bunch of gen_servers (as the event handlers).

I know I’m strange, but I think that the default behaviour of gen_event is perfectly fine, and ultimately much more flexible than the solution with concurrent handlers as suggested by Jose. If you want events to be handled concurrently, then you can trivally implement fanning out by installing event handlers that forward messages to existing processes or spawn processes to execute the event handling code. However, if conccurent handling of events is the default behaviour, how would you set about implementing sequential handling of events? I suspect that you’ll basically wind up implementing a less efficient version of gen_event; less efficient because you’ll be sending unnecessary messages.

Question: If gen_event ran handlers concurrently by default, how would you go about running handlers sequentially?

In conclusion, gen_event, much like Erlang’s message passing, provides a flexible base upon which you can easily implement different behaviour. I’m happy that gen_event exists and that it was decided that sequential handling of events would be the default behaviour.