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.
The following definitions will help in understand this topic.
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:
{ok, State}
. The new state is added to the MS list.
remove_handler
. The handler is finalized and then removed from the MS list.
{swap_handler, ...}
. The handler is finalized and the return value is passed into the init
function of the new handler.
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.
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:
Args
which is the reason for termination
State
which is the current value of the state.
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.
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
.
![]() |
We could also create a global event manager by calling |
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:
{ok, State}
. State
is a new state which will be used the next time the handler is called.
remove_handler
. This tells the event manager to remove this event handler by calling Handler:terminate(remove_handler, State)
.
{swap_handler, Args1, State1, Mod2, Args2}
. This tells the event manager to remove the current event handler by calling State2 = Handler:terminate(Args1, State1)
, and then to install a new handler by calling Mod2:init(Args2, State2)
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.
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.
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.
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.
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.
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:
gen_event:notify(plug_and_play, {added, Hw}).
Use this function to add hardware.
gen_event:notify(plug_and_play, {removed, Hw}).
Use this function to remove hardware.
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, []).
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 .
![]() |
This example supplies arguments to both |
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.