Event Handling in elixir
April 24, 2015
I was recently working on a project in elixir where I needed a way let other OTP applications subscribe to data updates from the application I was building. After asking around some I found GenEvent. I’ve used event handlers and event driven architecture with other languages, but I had not ventured into event handling with elixir yet.
The getting started guide on elixir-lang.org, has a basic intro to using GenEvent. You start a manager, attach a handler and then send events to the manager using the GenEvent.notify/2
or GenEvent.sync_notify/2
functions.
GenEvent
First we need to define our event handler, for this example we will define a simple handler that logs every event as an info message.
defmodule MyEventHandler do
use GenEvent
require Logger
def handle_event(event, parent) do
Logger.info("event received: #{inspect event}")
{:ok, parent}
end
end
To use our event handler we first need to start an event manager process. In elixir this can be done by starting a GenEvent process with start_link
iex(1)> {:ok, manager} = GenEvent.start_link
{:ok, #PID<0.55.0>}
Next we add our event handler MyEventHandler
to the event manager we just started.
iex(2)> GenEvent.add_handler(manager, MyEventHandler, self())
:ok
Finally we send an event to the manager for our event handler to process.
iex(3)> GenEvent.notify(manager, {:my_event, "testing"})
:ok
iex(4)>
00:00:00.000 [info] event received: {:my_event, "testing"}
That gets us through the basics of generating and handling our own custom events. Its a good intro, but after implementing the basics I wanted more information.
Looking at the elixir docs I saw the add_mon_handler/3
function. Since I’m writing this in elixir, it should be fault tolerant. And for my application this was a must.
Fault tolerance
Adding the event handler using the add_mon_handler/3
function will link it to the calling process. If an error occurs when the event handler is processing an event, a message will be sent from the event manager to the process that call add_mon_handler/3
.
I was curious how other people are using GenEvent
with a monitored event handler. So I started searching github for GenEvent.add_mon_handler
. I came across two common ways add_mon_handler/3
is being used.
Bare bones GenServer
defmodule MyEventHandlerWatcher do
def start_link(manager) do
GenServer.start_link(__MODULE__, manager, [])
end
def init(manager) do
:ok = GenEvent.add_mon_handler(manager, MyEventHandler, self())
{:ok, manager}
end
end
In the event that MyEventHandler
crashes, then MyEventHandlerWatcher
will also crash. As long as MyEventHandlerWatcher
is being supervised, then it will be restarted and re-add MyEventHandler
to the event manager. This will give a basic level of fault tolerance and ensures our event handler is restarted in the event of an error.
This approach is terse, but to me obfuscates what is happening. When MyEventHandler
crashes the event manager sends a message {:gen_event_EXIT, handler, reason}
to MyEventHandlerWatcher
. Since it does not handle this message, MyEventHandlerWatcher
will also crash and then be restarted by its supervisor. A side effect of this is and large unnecessary error message for the MyEventHandlerWatcher
crash.
Also this method only works if MyEventHandlerWatcher
does not use GenServer
. If it does then the :gen_event_EXIT
message will be swallowed and MyEventHandler
will not be re-added. This completely defeats the purpose of using add_mon_handler
over add_handler
.
Handling the event handler crash explicitly
Another approach, that I prefer, is to explicitly handle the
:gen_event_EXIT
message and re-add MyEventHanlder
to the event manager.
defmodule MyEventHandlerWatcher do
def start_link(manager) do
GenServer.start_link(__MODULE__, manager, [])
end
def init(manager) do
start_handler(manager)
{:ok, manager}
end
def start_handler(manager) do
:ok = GenEvent.add_mon_handler(manager, MyEventHandler, self())
end
def handle_info({:gen_event_EXIT, _handler, _reason}, manager) do
start_handler(manager)
{:ok, manager}
end
end
If you would like to test these approaches yourself, I have each implemented together in my gen event patterns repo. Follow the directions in the GenEventPatterns
moduledoc to work through the scenarios.
For more practical examples of GenEvent
and add_mon_handler
usage you can look at:
Also I wrote a simple GenServer module for adding monitored event handlers mon_handler code