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
instdlib
, 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:
- error logging
- alarm handling
- call record logging
- debugging
- equipment management.
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, andNewMod
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 eventEvent
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 withingen_event
. The callback functionsMod1:terminate(...)
andMod2: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 handlerThe 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 theinit
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 evaluatingMod:handle_call(Query, State)
.You remove a handler with the call
gen_event:delete_handler(EventManager, Mod, Args)
, which returns the value obtained by evaluatingMod:terminate(Args, State)
, whereState
is the state associated withMod
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 removedMod: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 terminationState
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.5.3 Writing an Event Manager
To create a new event manager, we evaluate the function
gen_event:start(Manager)
, whereManager
is the name of the event manager.For example, the call
gen_event:start({local, error_logger})
starts a new (local) event manager callederror_logger
. Note that callinggen_event:start({local, Manager})
has the side effect of creating a new registered process namedManager
.
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 handlerHandler
in the event managerManager
.When
gen_event:add_handler(Manager, Handler, Args)
is called, the event manager calls the functionHandler:init(Args)
which normally returns{ok,State}
. The value ofState
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 functionHandler:handle_event(Event, State)
. This is done for each handler which has been installed in the manager. The functionHandler: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 callingHandler:terminate(remove_handler, State)
.{swap_handler, Args1, State1, Mod2, Args2}
. This tells the event manager to remove the current event handler by callingState2 = Handler:terminate(Args1, State1)
, and then to install a new handler by callingMod2:init(Args2, State2)
5.3.1 An Error Logger
The module
error_logger_memory_h
provides a simple memory resident error logger. It stores at mostMax
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.
- If a file system is installed, then we might want to store the errors in disk files.
- If we have no file system, but we are a distributed Erlang node, then we might wish to send the errors to some other node in the system where they can be stored.
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}).
wherePid
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:
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, []).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 callgen_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
init
andterminate
.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 routinesstart/0
,stop/0
,added/1
,removed/1
andwhich/0
are added to the code forplug_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.