GenEvent

Introduction #

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 an 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/4. 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:

  • init(args) - invoked when the event handler is added.

    It must return:

    • {:ok, state}
    • {:ok, state, :hibernate}
    • {:error, reason}
  • handle_event(msg, state) - invoked whenever an event is sent via notify/2, ack_notify/2 or sync_notify/2.

    It must return:

    • {:ok, new_state}
    • {:ok, new_state, :hibernate}
    • {:swap_handler, args1, new_state, handler2, args2}
    • :remove_handler
  • handle_call(msg, state) - invoked when a call/3 is done to a specific handler.

    It must return:

    • {:ok, reply, new_state}
    • {:ok, reply, new_state, :hibernate}
    • {:swap_handler, reply, args1, new_state, handler2, args2}
    • {:remove_handler, reply}
  • handle_info(msg, state) - invoked to handle all other messages which are received by the process. Must return the same values as handle_event/2.

  • terminate(reason, state) - called when the event handler is removed or the event manager is terminating. It can return any term.

    The reason is one of:

    • :stop - manager is terminating
    • {:stop, reason} - monitored process terminated (for monitored handlers)
    • :remove_handler - handler is being removed
    • {:error, term} - handler crashed or returned a bad value
    • term - any term passed to functions like GenEvent.remove_handler/2
  • code_change(old_vsn, state, extra) - called when the application code is being upgraded live (hot code swapping).

    It must return:

    • {:ok, new_state}

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

GenEvents can be streamed from and streamed with the help of stream/2. Here are some examples:

stream = GenEvent.stream(pid)

# Take the next 10 events
Enum.take(stream, 10)

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

A stream may also be given an id, which allows all streams with the given id to be cancelled at any moment via cancel_streams/1.

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 monitor: true in GenEvent.add_handler/4.

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.

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

Source

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 :: module | {module, term}

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.

add_handler(manager, handler, args, options \\ [])

Specs

  • add_handler(manager, handler, term, [{:monitor, boolean}]) :: :ok | {:error, term}

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_added}.

Monitored handlers

When adding a handler, a :monitor option with value true can be given. This means the calling process will now be monitored by the GenEvent handler.

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).

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, :module_not_found}.

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.

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, :module_not_found}.

start(options \\ [])

Specs

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

See start_link/1 for more information.

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.

stop(manager)

Specs

Terminates the event manager.

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

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)

swap_handler(manager, handler1, args1, handler2, args2, options \\ [])

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, state}), where term is the return value of callingterminate/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}.

A :monitor option can also be set to specify if the new handler should be monitored by the manager. See add_handler/4 for more information.

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

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.

which_handlers(manager)

Specs

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