Log in | Back to darenet.org

ircd/api/events

In This Guide:

Overview

The IRC server is built around an event loop. Until the u2.10.11 release (which is what ircd-darenet 1.x is based off of), this event loop has been rather ad-hoc; timed events are hard-coded in, signals are handled inside the signal handler, etc. All of this changed with u2.10.11. A new subsystem, the events subsystem, was introduced; the new subsystem contains a generalization of the concept of an event. An event is a signal, the expiration of a timer, or some form of activity on a network socket. This new subsystem has the potential to vastly simplify the code that is arguably the core of any network program, and makes it much simpler to support more exotic forms of network activity monitoring than the conventional select() and poll() calls.

The primary concepts that the events subsystem works with are the "event," represented by a struct Event, and the "generator." There are three types of generators: sockets, represented by a struct Socket; signals, represented by a struct Signal; and timers, represented by struct Timer. Each of these generators will be described in turn.

Signals

The signal is perhaps the simplest generator in the entire events subsystem. Basically, instead of setting a signal handler, the function signal_add() is called, specifying a function to be called when a given signal is detected. Most importantly, that call-back function is called outside the context of a signal handler, permitting the call-back to use more exotic functions that are anathema within a signal handler, such as MyMalloc(). Once a call-back for a signal has been established, it cannot be deleted; this design decision was driven by the fact that ircd never changes its signal handlers.

Whenever a signal is received, an event of type ET_SIGNAL is generated, and that event is passed to the event call-back function specified in the signal_add() call.

Timers

Execution of the call-back functions for a timer occur when that timer expires; when a timer expires depends on the type of timer and the expiration time that was used for that timer. A TT_ABSOLUTE timer, for instance, expires at exactly the time given as the expiration time. This time is a standard UNIX time_t value, measuring seconds since the UNIX epoch. The TT_ABSOLUTE timer type is complemented by the TT_RELATIVE timer; the time passed as its expiration time is relative to the current time. If a TT_RELATIVE is given an expiration time of 5, for instance, it will expire 5 seconds after the present time. Internally, TT_RELATIVE timers are converted into TT_ABSOLUTE timers, with the expiration time adjusted by addition of the current time.

These two types of timers, TT_ABSOLUTE and TT_RELATIVE, are single-shot timers. Once they expire, they are removed from the timer list unless re-added by the event call-back or through some other mechanism. There is another type of timer, however, the TT_PERIODIC timer, that is not removed from the timer list. TT_PERIODIC timers are similar to TT_RELATIVE timers, in that one passes in the expire time as a relative number of seconds, but when they expire, they are re-added to the timer list with the same relative expire time. This means that a TT_PERIODIC timer with an expire time of 5 seconds that is set at 11:50:00 will have its call-back called at 11:50:05, 11:50:10, 11:50:15, and so on.

Timers have to be run by the event engines explicitly by calling timer_run() on the generator list passed to the engine event loop. In addition, engines may determine the next (absolute) time that a timer needs to be run by calling the time_next() macro; this may be used to set a timeout on the engine's network activity monitoring function. Engines are described in detail below.

When a timer expires, an event of ET_EXPIRE is generated, and the call-back function is called. When a timer is destroyed, either as the result of an expiration or as result of an explicit timer_del() call, am event of ET_DESTROY is generated, notifying the call-back that the struct Timer can be deallocated.

Sockets

Perhaps the most complicated event generator in all of the event subsystem is the socket, as described by struct Socket. This single classification covers datagram sockets and stream sockets. To differentiate the different kinds of sockets, there is a socket state associated with each socket. The available states are SS_CONNECTING, which indicates that a particular socket is in the process of completing a non-blocking connect(); SS_LISTENING, which indicates that a particular socket is a listening socket; SS_CONNECTED, which is the state of every other stream socket; SS_DATAGRAM, which is an ordinary datagram socket, and SS_CONNECTDG, which describes a connected datagram socket. The SS_NOTSOCK state is for the internal use of the event subsystem and will not be described here.

In addition to the socket states, there is also an event mask for each socket; this set of flags is used to tell the events subsystem what events the application is interested in for the socket. For SS_CONNECTING and SS_LISTENING sockets, this events mask has no meaning, but on the other socket states, the event mask is used to determine if the application is interested in readable (SOCK_EVENT_READABLE) or writable (SOCK_EVENT_WRITABLE) indications.

Most of the defined event types have to do with socket generators. When a socket turns up readable, for instance, an event type ET_READ is generated. Similarly, ET_WRITE is generated when a socket can be written to. The ET_ACCEPT event is generated when a listening socket indicates that there is a connection to be accepted; ET_CONNECT is generated when a non-blocking connect is completed. Finally, if an end-of-file indication is detected, ET_EOF is generated, whereas if an error has occurred on the socket, ET_ERROR is generated. Of course, when a socket has been deleted by the socket_del() function, an event of ET_DESTROY is generated when it is safe for the memory used by the struct Socket to be reclaimed.

Events

An event, represented by a struct Event, describes in detail all of the particulars of an event. Each event has a type, and an optional integer piece of data may be passed with some events -- in particular, ET_SIGNAL events pass the signal number, and ET_ERROR events pass the errno value. The struct Event also contains a pointer to the structure describing the generated event -- although it should be noted that the only way to disambiguate which type of generator is contained within the struct Event is by which call-back function has been called.

All generators have a void pointer which can be used to pass important information to the call-back, such as a pointer to a struct Client. Additionally, generators have a reference count, and a union of a void pointer and an integer that should only be utilized by the event engine. Finally, there is also a field for flags, although the only flag of concern to the application (or the engine) is the active flag, which may be tested using the test macros described below.

Whatever the generator, the call-back function is a function returning nothing (void) and taking as its sole argument a pointer to struct Event. This call-back function may be implemented as a single switch statement that calls out to appropriate external functions as needed.

Engines

Engines implement the actual socket event loop, and may also have some means of receiving signal events. Each engine has a name, which should describe what its core function is; for instance, the engine based on the standard select() function is named, simply, "select()." Each engine must implement several call-backs which are used to initialize the engine, notify the engine of sockets the application is interested in, etc. All of this data is described by a single struct Engine, which should be the only non-static variable or function in the engine's source file.

The engine's event loop, pointed to by the eng_loop field of the struct Engine, must consist of a single while loop predicated on the global variable running. Additionally, this loop's final statement must be a call to timer_run(), to execute all timers that have become due. Ideally, this construction should be pulled out of each engine's eng_loop and put in the event_loop() function of the events subsystem.

Reference Counts

As mentioned previously, all generators keep a reference count. Should timer_del() or socket_del() be called on a generator with a non-zero reference count, for whatever reason, the actual destruction of the generator will be delayed until the reference count again reaches zero. This is used by the event loop to keep sockets that it is currently referencing from being deallocated before it is done checking all pending events on them. To increment the reference count by one, call gen_ref_inc() on the generator; the corresponding macro gen_ref_dec() decrements the reference counts, and will automatically destroy the generator if the appropriate conditions are met.

Debugging Functions

It can be difficult to debug an engine if, say, a socket state can only be expressed as a meaningless number; therefore, when DEBUGMODE is #define'd, five number-to-name functions are also defined to make the debugging data more meaningful. These functions must only be called when DEBUGMODE is #define'd. Calling them from within Debug() macro calls is safe; calling them from log_write() calls is not.

Types, Enumerations, Structures, Macros and Functions

typedef void (*EventCallBack)(struct Event*);

The EventCallBack type is used to simplify declaration of event call-back functions. It is used in timer_add(), signal_add(), and socket_add(). The event call-back should process the event, taking whatever actions are necessary. The function should be declared as returning void.

typedef int (*EngineInit)(int);

The EngineInit function takes an integer specifying the maximum number of sockets the event system is expecting to handle. This number may be used by the engine initialization function for memory allocation computations. If initialization succeeds, this function must return 1. If initialization fails, the function should clean up after itself and return 0. The events subsystem has the ability to fall back upon another engine, should an engine initialization fail. Needless to say, the engines based upon poll() and select() should never fail in this way.