GenEvent

A behaviour module for implementing event handling functionality.

The event handling model consists of a generic event manager process with an arbitrary number of event handlers which are added and deleted dynamically.

An event manager implemented using this module will have a standard set of interface functions and include functionality for tracing and error reporting. It will also fit into a supervision tree.

Example

There are many use cases for event handlers. For example, a logging system can be built using event handlers where each log message is an event and different event handlers can be plugged to handle the log messages. One handler may print error messages on the terminal, another can write it to a file, while a third one can keep the messages in memory (like a buffer) until they are read.

As an example, let’s have a GenEvent that accumulates messages until they are collected by an explicit call.

defmodule LoggerHandler do
  use GenEvent

  # Callbacks

  def handle_event({:log, x}, messages) do
    {:ok, [x|messages]}
  end

  def handle_call(:messages, messages) do
    {:ok, Enum.reverse(messages), []}
  end
end

{:ok, pid} = GenEvent.start_link()

GenEvent.add_handler(pid, LoggerHandler, [])
#=> :ok

GenEvent.notify(pid, {:log, 1})
#=> :ok

GenEvent.notify(pid, {:log, 2})
#=> :ok

GenEvent.call(pid, LoggerHandler, :messages)
#=> [1, 2]

GenEvent.call(pid, LoggerHandler, :messages)
#=> []

We start a new event manager by calling GenEvent.start_link/0. Notifications can be sent to the event manager which will then invoke handle_event/2 for each registered handler.

We can add new handlers with add_handler/3 and add_mon_handler/3. Calls can also be made to specific handlers by using call/3.

Callbacks

There are 6 callbacks required to be implemented in a GenEvent. By adding use GenEvent to your module, Elixir will automatically define all 6 callbacks for you, leaving it up to you to implement the ones you want to customize. The callbacks are:

Name Registration

A GenEvent is bound to the same name registration rules as a GenServer. Read more about it in the GenServer docs.

Modes

GenEvent stream supports three different notifications.

On GenEvent.ack_notify/2, the manager acknowledges each event, providing back pressure, but processing of the message happens asynchronously.

On GenEvent.sync_notify/2, the manager acknowledges an event just after it was processed by all event handlers.

On GenEvent.notify/2, all events are processed asynchronously and there is no ack (which means there is no backpressure).

Streaming

GenEvent messages can be streamed with the help of stream/2. Here are some examples:

stream = GenEvent.stream(pid)

# Discard the next 10 events
_ = Enum.drop(stream, 10)

# Print all remaining events
for event <- stream do
  IO.inspect event
end

Learn more and compatibility

If you wish to find out more about gen events, Elixir getting started guides provide a tutorial-like introduction. The documentation and links in Erlang can also provide extra insight.

Keep in mind though Elixir and Erlang gen events are not 100% compatible. The :gen_event.add_sup_handler/3 is not supported by Elixir’s GenEvent, which in turn supports GenEvent.add_mon_handler/3.

The benefits of the monitoring approach are described in the “Don’t drink too much kool aid” section of the “Learn you some Erlang” link above. Due to those changes, Elixir’s GenEvent does not trap exits by default.

Furthermore, Elixir’s also normalizes the {:error, _} tuples returned by many functions, in order to be more consistent with themselves and the GenServer module.

Source

Summary

ack_notify(manager, event)

Sends a ack event notification to the event manager

add_handler(manager, handler, args)

Adds a new event handler to the event manager

add_mon_handler(manager, handler, args)

Adds a monitored event handler to the event manager

call(manager, handler, request, timeout \\ 5000)

Makes a synchronous call to the event handler installed in manager

notify(manager, event)

Sends an event notification to the event manager

remove_handler(manager, handler, args)

Removes an event handler from the event manager

start(options \\ [])

Starts an event manager process without links (outside of a supervision tree)

start_link(options \\ [])

Starts an event manager linked to the current process

stop(manager)

Terminates the event manager

stream(manager, options \\ [])

Returns a stream that consumes events from the manager

swap_handler(manager, handler1, args1, handler2, args2)

Replaces an old event handler with a new one in the event manager

swap_mon_handler(manager, handler1, args1, handler2, args2)

Replaces an old event handler with a new monitored one in the event manager

sync_notify(manager, event)

Sends a sync event notification to the event manager

which_handlers(manager)

Returns a list of all event handlers installed in the manager

Types

on_start :: {:ok, pid} | {:error, {:already_started, pid}}

Return values of start* functions

name :: atom | {:global, term} | {:via, module, term}

The GenEvent manager name

options :: [{:name, name}]

Options used by the start* functions

manager :: pid | name | {atom, node}

The event manager reference

handler :: atom | {atom, term} | {pid, reference}

Supported values for new handlers

Functions

ack_notify(manager, event)

Specs:

Sends a ack event notification to the event manager.

In other words, this function only returns :ok as soon as the event manager starts processing this event, but it does not wait for event handlers to process the sent event.

See notify/2 for more info. Note this function is specific to Elixir’s GenEvent and does not work with Erlang ones.

Source
add_handler(manager, handler, args)

Specs:

Adds a new event handler to the event manager.

The event manager will call the init/1 callback with args to initiate the event handler and its internal state.

If init/1 returns a correct value indicating successful completion, the event manager adds the event handler and this function returns :ok. If the callback fails with reason or returns {:error, reason}, the event handler is ignored and this function returns {:error, reason}.

If the given handler was previously installed at the manager, this function returns {:error, :already_present}.

Source
add_mon_handler(manager, handler, args)

Specs:

Adds a monitored event handler to the event manager.

Expects the same input and returns the same values as add_handler/3.

Monitored handlers

A monitored handler implies the calling process will now be monitored by the GenEvent manager.

If the calling process later terminates with reason, the event manager will delete the event handler by calling the terminate/2 callback with {:stop, reason} as argument. If the event handler later is deleted, the event manager sends a message {:gen_event_EXIT, handler, reason} to the calling process. Reason is one of the following:

  • :normal - if the event handler has been removed due to a call to remove_handler/3, or :remove_handler has been returned by a callback function

  • :shutdown - if the event handler has been removed because the event manager is terminating

  • {:swapped, new_handler, pid} - if the process pid has replaced the event handler by another

  • a term - if the event handler is removed due to an error. Which term depends on the error

Keep in mind that the {:gen_event_EXIT, handler, reason} message is not guaranteed to be delivered in case the manager crashes. If you want to guarantee the message is delivered, you have two options:

  • monitor the event manager
  • link to the event manager and then set Process.flag(:trap_exit, true) in your handler callback

Finally, this functionality only works with GenEvent started via this module (it is not backwards compatible with Erlang’s :gen_event).

Source
call(manager, handler, request, timeout \\ 5000)

Specs:

Makes a synchronous call to the event handler installed in manager.

The given request is sent and the caller waits until a reply arrives or a timeout occurs. The event manager will call handle_call/2 to handle the request.

The return value reply is defined in the return value of handle_call/2. If the specified event handler is not installed, the function returns {:error, :not_found}.

Source
notify(manager, event)

Specs:

Sends an event notification to the event manager.

The event manager will call handle_event/2 for each installed event handler.

notify is asynchronous and will return immediately after the notification is sent. notify will not fail even if the specified event manager does not exist, unless it is specified as an atom.

Source
remove_handler(manager, handler, args)

Specs:

Removes an event handler from the event manager.

The event manager will call terminate/2 to terminate the event handler and return the callback value. If the specified event handler is not installed, the function returns {:error, :not_found}.

Source
start(options \\ [])

Specs:

Starts an event manager process without links (outside of a supervision tree).

See start_link/1 for more information.

Source
start_link(options \\ [])

Specs:

Starts an event manager linked to the current process.

This is often used to start the GenEvent as part of a supervision tree.

It accepts the :name option which is described under the Name Registration section in the GenServer module docs.

If the event manager is successfully created and initialized, the function returns {:ok, pid}, where pid is the pid of the server. If there already exists a process with the specified server name, the function returns {:error, {:already_started, pid}} with the pid of that process.

Note that a GenEvent started with start_link/1 is linked to the parent process and will exit not only on crashes but also if the parent process exits with :normal reason.

Source
stop(manager)

Specs:

Terminates the event manager.

Before terminating, the event manager will call terminate(:stop, ...) for each installed event handler.

Source
stream(manager, options \\ [])

Specs:

Returns a stream that consumes events from the manager.

The stream is a GenEvent struct that implements the Enumerable protocol. Consumption of events only begins when enumeration starts.

Note streaming is specific to Elixir’s GenEvent and does not work with Erlang ones.

Options

  • :timeout - raises if no event arrives in X milliseconds (defaults to :infinity)
Source
swap_handler(manager, handler1, args1, handler2, args2)

Specs:

Replaces an old event handler with a new one in the event manager.

First, the old event handler is deleted by calling terminate/2 with the given args1 and collects the return value. Then the new event handler is added and initiated by calling init({args2, term}), where term is the return value of calling terminate/2 in the old handler. This makes it possible to transfer information from one handler to another.

The new handler will be added even if the specified old event handler is not installed or if the handler fails to terminate with a given reason in which case state = {:error, term}.

If init/1 in the second handler returns a correct value, this function returns :ok.

Source
swap_mon_handler(manager, handler1, args1, handler2, args2)

Specs:

Replaces an old event handler with a new monitored one in the event manager.

Read the docs for add_mon_handler/3 and swap_handler/5 for more information.

Source
sync_notify(manager, event)

Specs:

  • sync_notify(manager, term) :: :ok

Sends a sync event notification to the event manager.

In other words, this function only returns :ok after the event manager invokes the handle_event/2 on each installed event handler.

See notify/2 for more info.

Source
which_handlers(manager)

Specs:

Returns a list of all event handlers installed in the manager.

Source