[Erlang Systems]

5 Events

The event manager behaviour gen_event provides a general framework for building application specific event handling routines.

Refer to the Reference Manual , the module gen_event in stdlib, for full details of the behaviour interface.

Event managers provide named objects to which events can be sent. When an event arrives at an event manager, it will be processed by all the event handlers which have been installed within the event manager. None or several event handlers can be installed within a given event manager.

Event handlers can be written which act on all events in a particular class, on some of the events, or on some particular complex combination of events.

All events are processed by functions which are called from the module gen_event .

Event managers can be manipulated at runtime. In particular, we can install an event handler, remove an event handler, or replace one event handler with a different handler.

Event managers can be built for tasks like:

The event mechanism provides an extremely powerful model for building a large number of different applications. The following sections include examples of the kind of applications which can be built.

5.1 Definitions

The following definitions will help in understand this topic.

Event
An occurrence, or something which happens.
Event Category
The type or class of an event.
Event Manager
A process which coordinates the processing of events of the same category.
Notification
The act of informing an event manager that an event has occurred.
Event Handler
A module which exports functions that can process events of a particular category. Event handlers can be installed within an event manager.

5.2 The Event Manager

The event manager essentially maintains a list of {Mod, State} pairs, which are called an MS list. For example:

[{Mod1, State1}, {Mod2, State2}, ...]

New modules are added to this list by calling gen_event:add_handler(EventManager, NewMod, Args).

EventManager is the name of the event manager, and NewMod is the name of an event handler and its callback module.

The event manager calls NewMod:init(Args), which is expected to return {ok, NewState}. If this happens, the tuple {NewMod, NewState} is added to the MS list.

When an application generates an event by calling gen_event:notify(EventManager, Event), the event Event is delivered to the event manager.

The event manager then processes the event by calling Mod:handle_event(Event, State) for each module in the MS list. This has the effect of replacing the MS list [{Mod1, State1}, {Mod2, State2}, ....] with [{Mod1, State1p}, {Mod2, State2p}, ...], where:

{ok, State1p} = Mod1:handle_event(Event, State1)
{ok, State2p} = Mod2:handle_event(Event, State2)

The event manager can be thought of as a generalization of a conventional finite state machine. Instead of a single state, we maintain a set of states, and a set of state transition functions.

We further generalize this mechanism by allowing handle_event to return not only a new state, but also by allowing it to request a change of the event handler, or to request the removal of the existing event handler. What happens is shown by the following pseudo-code example which executes within gen_event. The callback functions Mod1:terminate(...) and Mod2:init(...) must also be supplied by the user.

notify(Event, Mod1, State) ->
     case Mod1:handle_event(Event, State) of
          {ok, State1} ->
               ... add  {Mod1, State1} to the MS list
          remove_handler ->
               Mod1:terminate(remove_handler, State),
               ... delete the handler from the MS list
          {swap_handler, Args1, State1, Mod2, Args2}
               State2 = Mod1:terminate(Args1, State1),
               {ok, State2a} = Mod2:init({Args2, State2}),
               ... add {Mod2, State2a} to the MS list and delete 
                                                the Mod1 handler

The handler returns the following values:

You can also send a request to a specific handler in the MS list by evaluating gen_event:call(EventManager, Mod, Query), which returns the value obtained by evaluating Mod:handle_call(Query, State).

You remove a handler with the call gen_event:delete_handler(EventManager, Mod, Args), which returns the value obtained by evaluating Mod:terminate(Args, State), where State is the state associated with Mod in the MS list.

5.2.1 Finalization

Each time a new handler is installed, Mod:init(...) is called, and each time a handler is removed Mod:terminate(...) is called.

The act of calling a specific routine every time a handler is removed is called "finalization". The finalization routine terminate has two arguments:

Mod:terminate/2 is expected to return a new state. Depending on the context, this state is sometimes ignored and sometimes passed into a new initialization routine.

5.3 Writing an Event Manager

To create a new event manager, we evaluate the function gen_event:start(Manager), where Manager is the name of the event manager.

For example, the call gen_event:start({local, error_logger}) starts a new (local) event manager called error_logger. Note that calling gen_event:start({local, Manager}) has the side effect of creating a new registered process named Manager.

Note!

We could also create a global event manager by calling gen_event:start({global, event_logger}).

So far, the error logger cannot do anything and we have to install a handler. The function gen_event:add_handler(Manager, Handler, Args) can be used to install the handler Handler in the event manager Manager.

When gen_event:add_handler(Manager, Handler, Args) is called, the event manager calls the function Handler:init(Args) which normally returns {ok,State}. The value of State is stored in the event manager together with the name of the handler.

Any process can send an event to the event manager by evaluating the function gen_event:notify(Manager, Event). When this happens, the event manager processes the event by calling the function Handler:handle_event(Event, State). This is done for each handler which has been installed in the manager. The function Handler:handle_event(Event, State) should return one of three different values:

5.3.1 An Error Logger

The module error_logger_memory_h provides a simple memory resident error logger. It stores at most Max error messages. After this all error messages are lost.

-module(error_logger_memory_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/roma/2 $ ').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_info/2, handle_call/2, terminate/2]).

init(Max) -> {ok, {Max, 0, []}}.

handle_event(Event, {1, Lost, Buff}) ->
    {ok, {1, Lost+1, Buff}};
handle_event(Event, {N, Lost, Buff}) ->
    {ok, {N-1, Lost, [{event1, date(), time(), Event}|Buff]}}.

handle_info(_, S) -> {ok, S}.

handle_call(_, S) -> {ok, ok, S}.

terminate(swap_to_file, {_, 0, Buff}) -> 
    {error_logger_memory_h, Buff};
terminate(swap_to_file, {_, Lost, Buff}) -> 
    {error_logger_memory_h, 
      [{event1,date(),time(),{Lost, messages_lost}}|Buff]};
terminate(_, State) ->
    ... display the data using a secret internal BIF ...
    ...


To start a simple memory based error logger which can store at most 25 messages we evaluate:

gen_event:start({local, error_logger}),
gen_event:add_handler(error_logger, error_logger_memory_h, 25).

To log an error, an application evaluates the expression:

gen_event:notify(error_logger, Event)

This error logger is similar to the error logger installed in the system kernel when the system boots. Be aware that no file system has been installed just after the system has started, so if any errors occur they are stored in memory. This error logger is perfectly adequate for recording errors which occur when booting the system.

The simple error logger shown can be improved by doing something more intelligent with the errors.

The following example shows a handler which stores events on disk:

-module(error_logger_file_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/3 $').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_info/2, handle_call/2, terminate/2]).

init({{Fname,Max,N}, {error_logger_memory_h, Buff}}) ->
     {ok, {{Fname, N}, length(Buff), Max, Buff}}.

handle_event(Event, {F, N, Max, Buff}) ->
     Buff1 = [{event1, date(), time(), Event}|Buff],
     N1 = N + 1,
     if
         N1 > Max -> {ok, {dump_events(F, Buff1), 0, Max, []}};
         true     -> {ok, {F, N1, Max, Buff1}}
     end.

handle_info(_, S) -> {ok, S}.

handle_call(_, S) -> {ok, ok, S}.

terminate(_, {F, N, Max, Buff}) ->
    dump_events(F, Buff),
    ok.

dump_events(F, []) ->F;
dump_events({File, Index}, Buff) ->
    Fname = File ++ integer_to_list(Index) ++ ".log",
    file:write_file(Fname, term_to_binary(Buff)),
    {File, Index + 1}.


This handler has been explicitly written to take over from the simple error handler. To swap handlers so that all errors are logged on disk we can evaluate:

gen_event:swap_handler(error_logger, 
                      {error_logger_memory_h, swap_to_file},
                       {file_error_handler_h, {"/usr/local/file/log",100,45}}).

Each disk file will contain 100 events. These files will be called /usr/local/file/log45.log, /usr/local/file/log46.log, and so on.

The reader should also examine this example carefully and observe the flow of control between the finalization routine in the memory resident error logger, and the initialization routine in the file logger.

5.3.2 An Alarm Handler

We start by creating an alarm manager.

gen_event:start({local, alarm}).

That is all you need to do to make an alarm manager. Any process can now generate an alarm by evaluating gen_event:notify(alarm, Event).

For example, to say that apparatus one is overheating you might call:

gen_event:notify(alarm, {hardware, 1, overheating}).

This alarm is then delivered to the alarm manager. However, the alarm manager will ignore the alarm since no alarm handlers have been installed.

The following example shows how to write and install an alarm handler which sends all alarms to the error logger:

-module(log_all_alarms_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/2 $').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_info/2, handle_call/2, terminate/2]).

init(_) -> {ok, 1}.

handle_event(Event, N) -> 
    gen_event:notify(error_logger, {alarm, N, Event}),
    {ok, N + 1}.

handle_info(_, S) -> {ok, S}.

handle_call(_,S) -> {ok, ok, S}.

terminate(_, _) -> ok.

The next example shows an alarm handler which is only interested in alarms from hardware1. This handler counts the alarms and stops the hardware if 10 alarms have arrived:

-module(hardware_1_alarms_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/roma/1 $').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_call/2, terminate/2]).

init(_) -> {ok, 1}.

handle_event({hardware, 1, What}, N) -> 
     N1 = N + 1,
     if
         N1 == 10 ->
             %% .... code to stop hardware 1 ....
             {ok, true};
         true ->
             {ok, N + 1}
     end;
handle_event(_,State) -> 
    %% This catches all other events intended
    %% for other handlers
    {ok, State}.

handle_call(_,State) -> {ok, ok, State}.

terminate(_, _) -> ok.

Both of these alarm handlers are installed in the alarm manager as follows:

gen_event:add_handler(alarm, log_all_alarms_h, []),
gen_event:add_handler(alarm, hardware_1_alarm_h, []),

Both handlers will run concurrently. A specialized handler can be added and removed at any time. Note also the second clause of handle_event. Since our handler must succeed for any event we add a final "catch all" clause and make sure it returns the original state.

5.3.3 Exit Notification

This section describes how to monitor a process and send a message to the error logger if the process terminates with an abnormal exit.

-module(at_exit_log_error_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/roma/1 $ ').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_info/2, handle_call/2, terminate/2]).

init(_) ->
    process_flag(trap_exit, true),
    {ok, []}.

handle_event({monitor, Pid}, S) -> 
    link(Pid), {ok, S};
handle_event(_, S) -> 
    {ok, S}.

handle_info({'EXIT', _, normal}, S) -> 
    {ok, S};
handle_info({'EXIT', Pid, Why}, S) -> 
    gen_event:notify(error_logger, {non_normal_exit, Pid, Why}),
    {ok, S};
handle_info(_, S) -> {ok, S}.

handle_call(_, S) -> {ok, ok, S}.

terminate(_, _) -> ok.

To start the handler we evaluate:

gen_event:start({local, at_exit}),
gen_event:add_handler(at_exit, at_exit_log_error_h, []).

A monitoring process is started with gen_event:notify(at_exit, {monitor, Pid}). where Pid represents the process that we wish to monitor.

5.3.4 Exit Handler

This section describes how to trigger an event to occur when a process exits.

-module(at_exit_apply_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/roma/1 $ ').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_info/2, handle_call/2, terminate/2]).

init(_) ->
    process_flag(trap_exit, true),
    {ok, []}.

handle_event({at_exit_apply,Pid,MFA},S) -> {ok, [{Pid, MFA}|S]};
handle_event(_, S)                      -> {ok, S}.

handle_info({'EXIT', Pid, _}, S) -> {ok, do_exit_actions(Pid,S,[])};
handle_info(_, S) -> {ok, S}.

handle_call(_, S) -> {ok, ok, S}.

terminate(_, _) -> [].

do_exit_actions(Pid, [{Pid, {M,F,A}}|T], L) ->
    catch apply(M, F, A),
    do_exit_actions(Pid, T, L);
do_exit_actions(Pid, [H|T], L) ->
    do_exit_actions(Pid, T, [H|L]);
do_exit_actions(Pid, [], L) ->
    L.

Install the handler as follows:

gen_event:start({local, at_exit}).
gen_event:add_handler(at_exit, at_exit_apply_h, []).

Set an event as follows:

gen_event:notify(at_exit, {at_exit, Pid, MFA})

Now, whenever Pid dies, MFA will be applied.

5.4 One or Many Handlers

The previous sections describe three different at_exit handlers. When designing a system we have to decide whether to install three different handlers in the same manager, or to create three different managers each with a single handler.

The following two examples produce the same effect.

Example 1:

gen_event:start({local, at_exit}).
gen_event:add_handler(at_exit, at_exit_apply_h, []).
gen_event:add_handler(at_exit, at_exit_log_error_h, []).
...
gen_event:notify(at_exit, {monitor, Pid}).
gen_event:notify(at_exit, {at_exit_apply, Pid, MFA}).

Example 2:

gen_event:start({local, at_exit_apply}).
gen_event:add_handler(at_exit_apply, at_exit_apply_h, []).
gen_event:start({local, at_exit_log_error}).
gen_event:add_handler(at_exit_log_error, at_exit_log_error_h, []).
...
gen_event:notify(at_exit_apply, {monitor, Pid}).
gen_event:notify(at_exit_log_error, {at_exit_apply, Pid, MFA}).

The first example creates one manager and installs two handlers. The second example creates two managers each with a single handler.

The first strategy is more flexible and will allow more handlers to be added at runtime, but at the cost of reducing concurrency in the system.

5.4.1 Plug and Play

This example assumes that a number of hardware drivers have been written which can automatically detect when hardware is added or removed from a system. The following functions are used to add and remove hardware from the system:

The handler plug_and_play_db_h maintains a database of all plug and play hardware which has been added to the system:

-module(plug_and_play_db_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/roma/1 $').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_info/2, handle_call/2, terminate/2]).

init(_) -> {ok, []}.

handle_event({added, Hw}, S)   -> {ok, [Hw|S]};
handle_event({removed, Hw}, S) -> {ok, lists:delete(Hw, S)};
handle_event(_, S)             -> {ok, S}.

handle_info(_, S) -> {ok, S}.

handle_call(what_hardware, State) -> {ok, State, State}.

terminate(_, _) -> ok.

This code just keeps a record of all hardware that has been started in a list. You can ask what hardware has been installed by evaluating the following function, which returns a list of the hardware that the plug and play manager knows about.

gen_event:call(plug_and_play, plug_and_play_db_h, what_hardware)

The following example shows a specialized handler which serves the purpose of doing something special when a piece of hardware is added, and doing something different when this piece of hardware is removed. The example is written for a sound card:

-module(plug_and_play_sound_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/2 $').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_info/2, handle_call/2, terminate/2]).

init(_) -> {ok, none}.

handle_event({added, {soundcard, X}}, S) ->
    Pid = soundcard:start(X),
    {ok, [{X, Pid}|S]};
handle_event({removed, {soundcard, X}}, S) ->
    {ok, stop_card(X, S, [])};
handle_event(_, S) ->
    {ok, S}.

stop_card(X, [{X, Pid}|T], L) -> soundcard:stop(Pid), lists:reverse(L, T);
stop_card(X, [H|T], L)        -> stop_card(X, T, [H|L]);
stop_card(X, [], L)           -> L.

handle_info(_, S) -> {ok, S}.

handle_call(_,S) -> {ok, ok, S}.

terminate(_,_) -> ok.


The plug-and-play manager can take care of both the plug-and-play database and the special processing of sound cards, when they are added and removed.

gen_event:start({local, plug_and_play}).
gen_event:add_handler(plug_and_play, plug_and_play_db_h, []).
gen_event:add_handler(plug_and_play, plug_and_play_sound_h, []).

5.4.2 Trace Logger

This section describes a simple handler which can trace all "foo" events.

-module(trace_foo_h).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/roma/1 $').

-behaviour(gen_event).

-export([init/1, handle_event/2, handle_info/2, handle_call/2, terminate/2]).

init(File) -> 
    {ok, Stream} = file:open(File, write),
    {ok, Stream}.

handle_event({foo, X}, F) -> io:format(F, "~w~n", [X]), {ok, F};
handle_event(_,S)         -> {ok, S}.

handle_info(_, S) -> {ok, S}.

handle_call(_,S) -> {ok, ok, S}.

terminate(_, S) -> file:close(S), ok.


If you start the tracer with the function gen_event:start({local, tracer}). and trace "foo" events with the call gen_event:notify(tracer, {foo, ...})., nothing will happen.

If you install a trace handler by calling gen_event:add_handler(tracer, trace_foo_h, "/usr/local/file1")., then all foo events will be written to the file "/usr/local/file1".

Evaluating gen_event:remove_handler(tracer, trace_foo_h). removes the handler and closes the file at the same time .

Note!

This example supplies arguments to both init and terminate.

5.5 Encapsulation

In all the examples shown in this section, gen_event function calls have been used instead of encapsulating the different functions which access the event manager. In the following example, the interface routines start/0, stop/0, added/1, removed/1 and which/0 are added to the code for plug_and_play.erl.

-module(plug_and_play).
-copyright('Copyright (c) 1991-97 Ericsson Telecom AB').
-vsn('$Revision: /main/release/2 $').

-behaviour(gen_event).

-export([start/0, stop/0, added/1, removed/1, which/0]).
-export([init/1, handle_event/2, handle_call/2, terminate/2]).

start() -> 
    gen_event:start({local, plug_and_play}),
    gen_event:add_handler(plug_and_play, plug_and_play, []).

stop()      -> gen_event:stop(plug_and_play).
added(Hw)   -> gen_event:notify(plug_and_play, {added, Hw}).
removed(Hw) -> gen_event:notify(plug_and_play, {removed, Hw}).
which()     -> gen_event:call(plug_and_play, plug_and_play, what_hardware).

init(_) -> {ok, []}.

handle_event({added, Hw}, S)      -> {state, [Hw|S]};
handle_event({removed_hw, Hw}, S) -> {state, lists:delete(Hw, S)};
handle_event(_, S)                -> {state, S}.

handle_call(what_hardware, State) -> {ok, State, State}.

terminate(_, _) -> ok.

This module should now be accessed through its interface routines only, and all details on how it was implemented using gen_event can be omitted.


Copyright © 1991-1999 Ericsson Utvecklings AB