3.4 The ACE Timer Queue Classes

Ru-Brd

Motivation

Many networked applications perform activities periodically or must be notified when specified time periods have elapsed. For example, Web servers require watchdog timers that release resources if clients don't send an HTTP GET request shortly after they connect.

Native OS timer capabilities vary, but many platforms share the following problems:

  • Limited number of timers. Many platforms allow applications to set a limited number of timers. For example, the POSIX alarm() and ualarm() system functions each reset a single "alarm clock" timer on each call. Managing multiple timer periods therefore often involves developing a timer queue mechanism that keeps track of the next scheduled expiration. Scheduling a new "earliest" timer can (re)set the alarm clock if necessary.

  • Timer expiration raises a signal. For example, the alarm() system function raises the SIGALRM signal when the timer expires . Programming timer signals is hard because application actions are restricted in signal context. Applications can minimize processing in signal context on UNIX platforms by using the sleep() system function or using the sigsuspend() system function. These solutions are nonportable, however, and they block the calling thread, which can impede concurrency and complicate programming.

One way to avoid these problems is to manage timers in the normal course of event handling, as follows :

  1. Develop a timer queue mechanism that orders timers and associates each timer with an action to perform when a timer expires

  2. Integrate the timer queue with the application's use of a synchronous event demultiplexer , such as select() or WaitForMultipleObjects() , to integrate the handling of timer expirations with other event processing.

It's hard to develop this type of timer facility portably across OS platforms, however, due to the wide range of capabilities and restrictions. Moreover, this capability is often redeveloped for many projects due to tight coupling between the timer queue mechanism and the synchronous event demultiplexing mechanism. To alleviate the need for application developers to rewrite efficient, scalable, and portable time-driven dispatchers in an ad hoc manner, the ACE Reactor framework defines a family of reusable timer queue classes.

Class Capabilities

The ACE timer queue classes allow applications to register time-driven event handlers derived from ACE_Event_Handler . These classes provide the following capabilities:

  • They allow applications to schedule event handlers whose handle_timeout() hook methods will be dispatched efficiently and scalably at caller-specified times in the future, either once or at periodic intervals.

  • They allow applications to cancel a timer associated with a particular event handler or all timers associated with an event handler.

  • They allow applications to configure a timer queue's time source, such as ACE_OS::gettimeofday() or ACE_High_Res_Timer::gettimeofday_hr() , described in Sidebar 14 (page 65).

The interfaces and relationships of all the ACE timer queue classes are shown in Figure 3.5 (page 64). The key methods in these classes are outlined in the following table:

Figure 3.5. The ACE Timer Queue Classes

Method

Description

schedule()

Schedule an event handler whose handle_timeout() method will be dispatched at a caller-specified time in the future and, optionally , at periodic intervals.

cancel()

Cancel a timer associated with a particular event handler or all timers associated with an event handler.

expire()

Dispatch the handle_timeout() method of all event handlers whose expiration time is less than or equal to the current time of day, which is represented as an absolute value, for example, 2001-09-11-09.37.00.

gettimeofday()

A pair of overloaded methods that allow applications to (1) set the method used to control the timer queue's source of the current time value and (2) call that method to return the current absolute time value.

The schedule() method of an ACE timer queue must be passed two parameters:

  • A pointer to an event handler that will be the target of the subsequent handle_timeout() dispatching and

  • A reference to an ACE_Time_Value indicating the absolute future time when the handle_timeout() hook method should be invoked on the event handler.

This method can optionally be passed the following parameters:

  • A void pointer that's stored internally by the timer queue and passed back unchanged when the handle_timeout() method is dispatched. This pointer can be used as an asynchronous completion token ( ACT ) in accordance with the Asynchronous Completion Token pattern [POSA2]. This pattern allows an application to efficiently demultiplex and process the responses of asynchronous operations it invokes on services. By using an ACT , the same event handler can be registered with a timer queue at multiple future dispatching times.

  • A reference to a second ACE_Time_Value that designates the interval at which the event handler should be dispatched periodically. If this parameter is omitted, the event handler's handle_timeout() method is dispatched only once. When specifying very short timer values, keep in mind that the actual timer resolution will be limited to that of the timer queue's time source clock update frequency, which varies by platform.

When a timer queue dispatches an event handler's handle_timeout() method, it passes as parameters the current time and the void pointer ACT passed to the schedule() method when the event handler was scheduled originally.

The return value of schedule() uniquely identifies each timer event that's scheduled with a timer queue. Applications can pass the unique timer identifier to the cancel() method to remove a particular event handler before it expires. Applications can also pass the address of the handler to cancel() to remove all timers associated with a particular event handler. If a non- NULL pointer to a void pointer is passed to cancel() , it's assigned the ACT passed by the application when the timer was scheduled originally, which makes it possible to allocate an ACT dynamically without leaking memory.

The ACE timer queue classes combine design patterns, hook methods, and template arguments to provide the following timer queue implementations :

  • ACE_Timer_Heap , which is a partially ordered, almost complete binary tree implemented in an array [Rob99]. Its average- and worst-case performance for scheduling, canceling, and expiring an event handler is O (lg n ), which is why it's the default timer queue mechanism in the ACE Reactor framework. Heap-based timer queues are useful for operating systems [BL88] and middleware [SLM98] whose applications require predictable and low-latency time-driven dispatching. Sidebar 15 (page 66) describes some issues that developers of real-time applications should consider when using the ACE timer queue mechanisms in their code.

  • ACE_Timer_Wheel , which uses a timing wheel [VL97] that contains a circular buffer designed to schedule, cancel, and dispatch timers in O (1) time in the average case, but O ( n ) in the worst case.

    Sidebar 14: ACE Time Sources

    Figure 3.5 shows that ACE_Timer_Queue::gettimeofday() can be changed to use any static method that returns an ACE_Time_Value value. The returned time value is expected to be absolute, but it need not be the current date and time (commonly known as "wall clock" time). The primary requirement of the time-returning static method is that it provides an accurate basis for timer scheduling and expiration decisions. ACE provides two mechanisms that fit this requirement:

    • ACE_OS::gettimeofday() , which is a static method that returns an ACE_Time_Value containing the current absolute date and time as reported by the operating system.

    • ACE_High_Res_Timer::gettimeofday_hr() , which is a static method that returns the value of an OS-specific high-resolution timer, converted to ACE_Time_Value units. These timers are often based on the number of CPU clock ticks since boot time, rather than the actual wall clock time.

    The granularity of these timers can differ by three to four orders of magnitude. When they're used to time out event demultiplexing mechanisms, however, the resolution is usually similar due to the intricacies of clocks, timer interrupt servicing , and OS scheduling. In the context of the Reactor's timer queue, they differ mainly in their behavior when the system's date and/or time is changed, where the values reported by ACE_OS::gettimeofday() will change but the values reported by ACE_High_Res_Timer::gettimeofday_hr() won't be affected ”they'll keep increasing at a constant rate. If an application's timer behavior must remain constant ” even if the wall clock time changes ”the default time source should be replaced with ACE_High_Res_Timer::gettimeofday_hr() , as shown in the Example portion of Section 3.4.

  • ACE_Timer_Hash , which uses a hash table to manage the queue. Like the timing wheel implementation, the average-case time required to schedule, cancel, and expire timers is O (1) and its worst-case is O ( n ).

  • ACE_Timer_List , which is implemented as a linked list of absolute timers ordered by increasing deadline. Although its average and worst-case performance for scheduling and canceling timers is O ( n ), it uses the least amount of memory of the ACE timer queue implementations.

The ACE Reactor framework allows developers to use any of these timer queue implementations to achieve the functionality needed by their applications, without burdening them with a "one size fits all" implementation. Since the methods in the ACE_Timer_Queue base class are virtual, applications can also integrate their own implementations of other timer queue mechanisms.

Sidebar 15: Using Timers in Networked Real-time Applications

Networked real-time applications are becoming increasingly widespread and important [GSC02]. Common examples include telecommunication networks (e.g., wireless phone services), telemedicine (e.g., remote surgery), manufacturing process automation (e.g., hot rolling mills), and defense applications (e.g., avionics mission computing systems). These types of applications typically have one thing in common: the right answer delivered too late becomes the wrong answer.

If one reactor is used to dispatch both I/O and timer queue handlers, variations in the amount of time spent handling I/O can cause the timer queue processing to " drift ," which can wreak havoc on the scheduling analysis and predictability of networked real-time applications. Moreover, the event demultiplexing and synchronization mechanisms used to integrate I/O and timer dispatching in implementations of the ACE Reactor framework can incur more overhead than some real-time applications can afford.

Developers of networked real-time applications may therefore want to use separate ACE timer queues running in different threads to handle timer dispatching. Depending on the relative importance of timers versus I/O, different thread priorities can be used for the different threads. The ACE_Thread_Timer_Queue_Adapter is a useful class that can simplify the design of these networked real-time applications.

Example

Although the Logging_Acceptor and Logging_Event_Handler event handlers in Section 3.3 implement the logging server functionality correctly, they may consume system resources unnecessarily. For example, clients can connect to a server and then not send log records for long periods of time. In the example below, we illustrate how to apply the ACE timer queue mechanisms to reclaim resources from those event handlers whose clients log records infrequently. Our design is based on the Evictor pattern [HV99], which describes how and when to release resources, such as memory and I/O handles, in order to optimize system resource management.

We use the Evictor pattern in conjunction with the ACE Reactor framework's timer queue mechanisms to check periodically when each registered event handler has received its last client log record. If the time the last log record was received exceeds a designated threshold (one hour ), the following actions occur:

  1. The event handler is disconnected from its client.

  2. Its resources are returned to the OS.

  3. It is removed from the reactor.

Clients must detect these closed connections and reestablish them when they need to send more log records, as shown in the Example portions of Sections 6.2 and 7.4.

This example uses ACE_Timer_Queue::gettimeofday() to obtain all time-of-day values. This method allows us to change the program's time source at run time as described in Sidebar 14 (page 65). For example, if changes to the system's time-of-day clock should neither delay nor hasten client evictions, the following code can be added to main() before any timer- related work is done:

 ACE_Reactor *r = ACE_Reactor::instance ();  ACE_High_Res_Timer::global_scale_factor ();  r->timer_queue ()->    gettimeofday (&ACE_High_Res_Timer::gettimeofday_hr); 

The ACE_High_Res_Timer::global_scale_factor() call performs any initialization needed to obtain accurate timer values before the program starts doing real work.

To implement the Evictor pattern, we extend Logging_Acceptor and Logging_Event_Handler in Section 3.3 to create Logging_Acceptor_Ex and Logging_Event_Handler_Ex . We then register a timer for every instance of Logging_Event_Handler_Ex . Since the default ACE timer queue ( ACE_Timer_Heap ) is highly scalable and efficient, its scheduling, cancellation, and dispatching overhead is low, even if there are thousands of timers and instances of Logging_Event_Handler_Ex .

We start by creating a new header file called Logging_Acceptor_Ex.h that contains the new Logging_Acceptor_Ex class. The changes to this class are minor. We simply override and modify its handle_input() method to create a Logging_Event_Handler_Ex rather than a Logging_Event_Handler , as shown below:

 #include "ace/INET_Addr.h"  #include "ace/Reactor.h"  #include "Logging_Acceptor.h"  #include "Logging_Event_Handler_Ex.h"  class Logging_Acceptor_Ex : public Logging_Acceptor {  public:    typedef ACE_INET_Addr PEER_ADDR;    // Simple constructor to pass <ACE_Reactor> to base class.    Logging_Acceptor_Ex (ACE_Reactor *r = ACE_Reactor::instance ())      : Logging_Acceptor (r) {}    int handle_input (ACE_HANDLE) {      Logging_Event_Handler_Ex *peer_handler = 0;      ACE_NEW_RETURN (peer_handler,                      Logging_Event_Handler_Ex (reactor ()), -1);      // ... same as Logging_Acceptor::handle_input()    }  }; 

In Chapter 7, we'll illustrate how the ACE Acceptor-Connector framework uses C++ features (such as templates, inheritance, and dynamic binding) and design patterns (such as Template Method and Strategy [GoF]) to add new behavior to an event handler without copying or modifying existing code.

The Logging_Event_Handler_Ex class involves more substantial additions, so we create both a Logging_Event_Handler_Ex.h file and a Logging_Event_Handler_Ex.cpp file. In Logging_Event_Handler_Ex.h we extend Logging_Event_Handler to create the following Logging_Event_Handler_Ex class:

 class Logging_Event_Handler_Ex : public Logging_Event_Handler {  private:    // Time when a client last sent a log record.    ACE_Time_Value time_of_last_log_record_;    // Maximum time to wait for a client log record.    const ACE_Time_Value max_client_timeout_; 

We implement the Evictor pattern by adding an ACE_Time_Value that keeps track of the time when a client last sent a log record. The methods in the public interface of Logging_Event_Handler_Ex are shown below:

 public:    typedef Logging_Event_Handler PARENT;    // 3600 seconds == one hour.    enum { MAX_CLIENT_TIMEOUT = 3600 };    Logging_Event_Handler_Ex        (ACE_Reactor *reactor,         const ACE_Time_Value &max_client_timeout           = ACE_Time_Value (MAX_CLIENT_TIMEOUT))      : Logging_Event_Handler (reactor),        time_of_last_log_record (0),        max_client_timeout_ (max_client_timeout) {}    virtual Logging_Event_Handler_Ex () { /* no-op */ }    virtual int open (); // Activate the event handler.    // Called by a reactor when logging events arrive.    virtual int handle_input (ACE_HANDLE);    // Called when a timeout expires to check if the client has    // been idle for an excessive amount of time.    virtual int handle_timeout (const ACE_Time_Value &tv,                                const void *act);    // Called when this object is destroyed, e.g., when it's    // removed from a reactor.    virtual int handle_close (ACE_HANDLE = ACE_INVALID_HANDLE,                              ACE_Reactor_Mask = 0);  }; 

The Logging_Event_Handler_Ex::handle_input() method notes the time when a log record is received from the connected client. We get the time from the timer queue's time source to ensure that all comparisons are consistent. After recording the time, this method forwards to its parent's handle_input() method to process the log record, as shown below:

 int Logging_Event_Handler_Ex::handle_input (ACE_HANDLE h) {    time_of_last_log_record_ =      reactor ()->timer_queue ()->gettimeofday ();    return PARENT::handle_input (h);  } 

The open() method is shown next:

 1 int Logging_Event_Handler_Ex::open () {   2   int result = PARENT::open ();   3   if (result != -1) {   4     ACE_Time_Value reschedule (max_client_timeout_.sec () / 4);   5     result = reactor ()->schedule_timer   6                (this, 0,   7                 max_client_timeout_,  // Initial timeout.   8                 reschedule);          // Subsequent timeouts.   9   }  10   return result;  11 } 

Line 2 Forward to the parent's open() method (page 59).

Lines 4 “8 Call ACE_Reactor::schedule_timer() (page 76) to schedule this event handler to be dispatched by the reactor periodically to check whether its client sent it a log record recently. We schedule the initial timer to expire in max_client_timeout_ seconds (which defaults to one hour) and also request that it then expire periodically every max_client_timeout_ / 4 seconds (i.e., check every 15 minutes).

When a timer expires, the reactor uses its timer queue mechanism to dispatch the following handle_timeout() hook method automatically:

 int Logging_Event_Handler_Ex::handle_timeout      (const ACE_Time_Value &now, const void *) {    if (now - time_of_last_log_record_ >= max_client_timeout_)      reactor ()->remove_handler (this,                                  ACE_Event_Handler::READ_MASK);    return 0;  } 

When an ACE timer queue dispatches handle_timeout() , it sets the now parameter to the absolute time (from ACE_Timer_Queue::gettimeofday() ) when the queue expires the timer. Logging_Event_Handler_Ex::handle_timeout() checks if the time elapsed between now and the time when this event handler received its last log record is greater than the designated max_client_timeout_ threshold. If so, it calls ACE_Reactor::remove_handler() , which triggers the reactor to call the following hook method to remove the event handler from the reactor:

 int Logging_Event_Handler_Ex::handle_close (ACE_HANDLE,                                              ACE_Reactor_Mask) {    reactor ()->cancel_timer (this);    return PARENT::handle_close ();  } 

This method cancels the handler's timer and calls its parent's handle_close() method (page 60), which closes the log file and socket to the client, and then deletes itself.

Ru-Brd


C++ Network Programming
C++ Network Programming, Volume I: Mastering Complexity with ACE and Patterns
ISBN: 0201604647
EAN: 2147483647
Year: 2002
Pages: 65

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net