As you all know, I've been working on rewriting ircu's event loop--in a seperate branch, so as to avoid destabilizing the present alpha. I finished that work a few days ago, and began testing. I am pleased to report that I've managed to work out most of the major bugs in the overall event system, and that it now appears quite stable. I'll be checking a few more aspects of the thing today--namely, signal handling--and probably adjust some of the debug logging--this thing can dump millions of lines of debug logs when only mildly active. When I come back, I'll work on debugging the other engines, then I'll begin the work of merging it into the main-line. Once it's merged, I have a few other TODO items--including writing documentation, which I'm sure most of you are looking forward to. I'm going to take this opportunity to describe the architecture of the new events system. The old code wasn't truly an event loop at all; it did some timing things, then called to either a poll()- or select()-based routine that went through the list of on-line clients, called poll() or select(), then went through that list again. The core event loop of the new system is incredibly simple compared to that, although complexity has been introduced elsewhere. The event loop handles--er, events. An "event" is a signal received by the server, such as SIGTERM; a socket that's become readable or writable; or a timer that has expired. When one of these events occur, a structure is allocated that describes the event, and a callback is called. (There is code that allows these events to be placed into a queue instead of being called immediately; this is to allow for going with a threaded "work-crew" model--in the distant future.) The callback can do whatever actions are necessary to process the event, such as calling accept() on the socket. The event structures do contain pointers to a structure that describes the thing on which they occurred; this structure is referred to as the "generator." There are three types of generators, as mentioned briefly above. A struct Signal describes a signal generator. Whenever a signal is received, an event of type ET_SIGNAL is generated and processed by the callback. The callback routine is not subject to the limited environment of the signal handler, and so can safely call any function. Needless to say, the only three signals that actually do anything are HUP, INT, and TERM, and they do what they've always done--they just do it better. The second type of generator I'll describe is the struct Timer generator. The code supports three types of timers: TT_ABSOLUTE timers expire on (or possibly after) an exact time_t value; TT_RELATIVE timers expire after so many seconds; and TT_PERIODIC timers expire after so many seconds, and are then re-queued. For example, if I added a TT_ABSOLUTE timer with an expire time of 990799386, this timer would be run at Fri May 25 10:03:06 2001 (EDT). If I added a TT_RELATIVE timer for 60 seconds, when the current time is 990799386, it will expire 60 seconds later, at 990799446. Finally, if I added a TT_PERIODIC timer for 60 seconds, when the current time is 990799386, it will run at 990799446, 990799506, 990799566, and so on. When a timer expires, an event of type ET_EXPIRE is generated. Since all of these structures are allocated by the caller of timer_add() (or socket_add() or signal_add()), when a timer is deleted or expires (and is not to be re-added), an event of type ET_DESTROY will be generated to allow the event callback to de-allocate the structure. The third type of generator is the most complex--it is described by struct Socket. Sockets can be in one of several states. A socket which has had a connection initiated on it, such as an autoconnect to another server or a connect to a remote identd, is in state SS_CONNECTING. If the socket is a listening socket, then it's in state SS_LISTENING. A socket which has been accepted from a listening socket, or on which a connection has completed, is in state SS_CONNECTED--this is a normal, complete client (or server) connection. Finally, there's SS_DATAGRAM, for ordinary datagram sockets, and SS_CONNECTDG for datagram sockets we've called connect() on. The socket states are mostly for the convenience of the caller, but they do have an effect on what event type is generated. For instance, SS_CONNECTING sockets generate ET_CONNECT events when the connection completes, and SS_LISTENING sockets generate ET_ACCEPT when there's a connection to be accepted. The remaining three socket states simply generate ordinary ET_READ and ET_WRITE events. Sockets also have an event mask that indicates to the event loop which events we're presently interested in. We don't have to do anything special for SS_CONNECTING or SS_LISTENING sockets--the event loop automatically figures out what the correct indication for ET_CONNECT or ET_ACCEPT looks like. However, this event mask allows us to tell the engine that we're not ready to accept data from the client yet (for instance). It also means that we don't have to check for writable indications unless we actually have data to write. The final piece of the architecture I want to cover is the engines. The engines are the core of the whole events system; they wrap around calls to such functions as poll() and select(). If we want to support another type of interface, say, FreeBSD's kqueue() interface, we just write a single .c file that implements that engine. The system, as written, has 4 engines-- poll(), select(), kqueue(), and one based on /dev/poll. Moreover, there is a fall-back behavior; if, say, the /dev/poll engine cannot be initialized, for whatever reason, the event system will fall back to either a poll()- or select()-based engine. The engines are designed for high efficiency. For instance, the poll()- based engine allocates an array of struct pollfd as part of its initialization; whenever you change a socket state or a socket event mask, a callback in the engine makes the appropriate change to the appropriate element of the struct pollfd array. This means that the engine's event loop need only loop through the elements in the struct pollfd array once; the array it needs to feed to poll() has already been computed. The select()-based engine does something similar with fd_set's, and uses the magic of C's structure copy to keep the statically maintained fd_set's from being changed inside of select(). The kqueue()- and /dev/poll-based engines don't even have to be fed new structures or fd_set's, as the interfaces already have the same update features as the API for the engines. I have put a lot of work into coming up with and implementing this architecture. It's working quite well now on my test server right now. If we can debug the kqueue()-based engine before releasing u2.10.11, we may very well see the CPU load on FreeBSD-based irc servers drop quite considerably, given the statistics on http://www.kegel.com/c10k.html. I hope you like it. -- Kevin L. Mitchell <[EMAIL PROTECTED]>