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},
receive
{Ref, Reply} -> Reply
end.
async_recv() ->
receive
M -> {ok, M}
after
0 -> none
end.
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.