Quick Links:

bal | bbl | bdl | bsl

Namespaces

Component bdlmt_eventscheduler
[Package bdlmt]

Provide a thread-safe recurring and one-time event scheduler. More...

Namespaces

namespace  bdlmt

Detailed Description

Outline
Purpose:
Provide a thread-safe recurring and one-time event scheduler.
Classes:
bdlmt::EventScheduler a thread-safe event scheduler
bdlmt::EventSchedulerEventHandle handle to a single scheduled event
bdlmt::EventSchedulerRecurringEventHandle handle to a recurring event
bdlmt::EventSchedulerTestTimeSource class for testing time changes
See also:
Component bdlmt_timereventscheduler
Description:
This component provides a thread-safe event scheduler. bdlmt::EventScheduler, that implements methods to schedule and cancel recurring and one-time events. All of the callbacks for these events are processed by a separate thread (called the dispatcher thread). By default the callbacks are also executed in the dispatcher thread, but that behavior can be altered by providing a dispatcher functor at creation time (see the section The Dispatcher Thread and the Dispatcher Functor).
Events may be referred to by bdlmt::EventSchedulerEventHandle and bdlmt::EventSchedulerRecurringEventHandle objects, which clean up after themselves when they go out of scope, or by Event and RecurringEvent pointers, which must be released using releaseEventRaw. Such pointers are used in the "Raw" API of this class and must be used carefully. Note that the Handle objects have an implicit conversion to the corresponding Event or RecurringEvent pointer types, effectively providing extra overloads for methods that take a const Event* to also take a const EventHandle&.
Comparison to bdlmt::TimerEventScheduler:
This component was written after bdlmt_timereventscheduler, which suffered from a couple of short-comings: 1) there was a maximum number of events it could manage, and 2) it was inefficient at dealing with large numbers of events. This component addresses both those problems -- there is no limit on the number of events it can manage, and it is more efficient at dealing with large numbers of events. The disadvantage of this component relative to bdlmt_timereventscheduler is that handles referring to managed events in a bdlmt::EventScheduler are reference-counted and need to be released, while handles of events in a bdlmt::TimerEventScheduler are integral types that do not need to be released.
Thread Safety and "Raw" Event Pointers:
bdlmt::EventScheduler is thread-safe and thread-enabled, meaning that multiple threads may use their own instances of the class or use a shared instance without further synchronization. The thread safety and correct behavior of the component depend on the correct usage of Event pointers, which refer to scheduled events in the "Raw" API of this class. In particular:
 Every 'Event*'  and 'RecurringEvent*' populated by 'scheduleEventRaw'
 and 'scheduleRecurringEventRaw' must be released using 'releaseEventRaw.'
       - Pointers are not released automatically when events are completed.
       - Pointers are not released automatically when events are canceled.
       - Events are not canceled when pointers to them are released.
 Pointers must not be used after being released.
 Pointers must never be shared or duplicated without using
    'addEventRefRaw' and 'addRecurringEventRefRaw' to get additional
     references; *each* such added reference must be released separately.
bdlmt::EventSchedulerEventHandle and bdlmt::EventSchedulerRecurringEventHandle are const thread-safe. It is not safe for multiple threads to invoke non-'const' methods on the same EventHandle or RecurringEventHandle object concurrently.
The Dispatcher Thread and the Dispatcher Functor:
The scheduler creates a single separate thread (called the dispatcher thread) to process all the callbacks. The dispatcher thread executes the callbacks by passing them to the dispatcher functor (optionally specified at creation time). The default dispatcher functor simply invokes the passed callback, effectively executing it in the dispatcher thread. Users can alter this behavior by defining their own dispatcher functor (for example in order to use a thread pool or a separate thread to run the callbacks). Note that the user-supplied functor will still be run in the dispatcher thread.
CAVEAT: Using a dispatcher functor such as the example above (to execute the callback in a separate thread) violates the guarantees of cancelEventAndWait(). Users who specify a dispatcher functor that transfers the event to another thread for execution should not use cancelEventAndWait(), and should instead ensure that the lifetime of any object bound to an event exceeds the lifetime of the mechanism used by the customized dispatcher functor.
Timer Resolution and Order of Execution:
It is intended that recurring and one-time events are processed as closely as possible to their respective time values, and that they are processed in the order scheduled. However, this component guarantees only that events will not be executed before their scheduled time. Generally, events that are scheduled more than 1 microsecond apart will be executed in the order scheduled; but different behavior may be observed when events are submitted after (or shortly before) their scheduled time.
When events are executed in the dispatcher thread and take longer to complete than the time between events, the dispatcher can fall behind. In this case, events will be executed in the correct order as soon as the dispatcher thread becomes available; once the backlog is worked off, events will be executed at or near their scheduled times.
Supported Clock Types:
An EventScheduler optionally accepts a clock type at construction indicating the clock by which it will internally schedule events. The clock type may be indicated by either a bsls::SystemClockType::Enum value, a bsl::chrono::system_clock object (which is equivalent to specifying e_REALTIME), or a bsl::chrono::steady_clock object (equivalent to specifying e_MONOTONIC). If a clock type is not specified, e_REALTIME is used.
Scheduling Using a bsl::chrono::time_point:
When creating either a one-time or recurring event, clients may pass a bsl::chrono::time_point indicating the time the event should occur. This time_point object can be associated with an arbitrary clock. If the time_point is associated with a different clock than was indicated at construction of the event scheduler, those time points are converted to be relative to the event scheduler's clock for processing. A possible implementation of such a conversion would be:
  bsls::TimeInterval(time - t_CLOCK::now()) + eventScheduler.now()
where time is a time_point, t_CLOCK is the clock associated with time, and eventScheduler is the EventScheduler on which the event is being scheduled. Notice that the conversion adds some imprecision and overhead to evaluation of the event. An event scheduler guarantees an event will occur at or after the supplied time_point, even if that time_point is defined in terms of a t_CLOCK different from the one used by the event scheduler. If there is a discontinuity between the clock for a time_point and the event scheduler's clock, additional processing overhead may result (because the event may need to be rescheduled), and the event may also occur later than what one might otherwise expect.
Scheduling Using a bsls::TimeInterval:
When creating either a one-time or recurring event, clients may pass a bsls::TimeInterval indicating the time the event should occur as an offset from an epoch. If the clock type indicated at construction is bsls::SystemClockType::e_REALTIME, time should be expressed as an absolute offset since 00:00:00 UTC, January 1, 1970 (which matches the epoch used in bdlt::CurrentTime::now(bsls::SystemClockType::e_REALTIME), and bsl::chrono::system_clock::now()). If the clock type indicated at construction is bsls::SystemClockType::e_MONOTONIC, time should be expressed as an absolute offset since the epoch of this clock (which matches the epoch used in bdlt::CurrentTime::now(bsls::SystemClockType::e_MONOTONIC) and bsl::chrono::steady_clock).
Event Clock Substitution:
For testing purposes, a class bdlmt::EventSchedulerTestTimeSource is provided to allow manual manipulation of the system-time observed by a bdlmt::EventScheduler. A test driver that interacts with a bdlmt::EventScheduler can use a bdlmt::EventSchedulerTestTimeSource object to control when scheduled events are triggered, allowing more reliable tests.
A bdlmt::EventSchedulerTestTimeSource can be constructed for any existing bdlmt::EventScheduler object that has not been started and has not had any events scheduled. When the bdlmt::EventSchedulerTestTimeSource is constructed, it will replace the clock of the bdlmt::EventScheduler to which it is attached. The internal clock of the bdlmt::EventSchedulerTestTimeSource will be initialized with an arbitrary value on construction, and will advance only when explicitly instructed to do so by a call to bdlt::EventSchedulerTestTimeSource::advanceTime. The current value of the internal clock can be accessed by calling bdlt::EventSchedulerTestTimeSource::now, or bdlmt::EventScheduler::now on the instance supplied to the bdlmt::EventSchedulerTestTimeSource.
Note that the initial value of bdlt::EventSchedulerTestTimeSource::now is intentionally not synchronized with bsls::SystemTime::nowRealtimeClock. All test events scheduled for a bdlmt::EventScheduler that is instrumented with a bdlt::EventSchedulerTestTimeSource should be scheduled in terms of an offset from whatever arbitrary time is reported by bdlt::EventSchedulerTestTimeSource. See Example 3 below for an illustration of how this is done.
Thread Name for Dispatcher Thread:
To facilitate debugging, users can provide a thread name as the threadName attribute of the bslmt::ThreadAttributes argument passed to the start method, that will be used for the dispatcher thread. The thread name should not be used programmatically, but will appear in debugging tools on platforms that support naming threads to help users identify the source and purpose of a thread. If no ThreadAttributes object is passed, or if the threadName attribute is not set, the default value "bdl.EventSched" will be used.
Usage:
This section illustrates intended use of this component.
Example 1: Simple Clock:
In this example we wish to log some statistics periodically. We define a method to store the value of a variable into an array, and set up a scheduler to call that as a recurring event.
   bsls::AtomicInt  g_data;  // Some global data we want to track
   typedef pair<bsls::TimeInterval, int> Value;

   void saveData(vector<Value> *array)
   {
      array->push_back(Value(bsls::SystemTime::nowRealtimeClock(), g_data));
   }
We allow the scheduler to run for a short time while changing this value and observe that the callback is executed:
   bdlmt::EventScheduler scheduler;
   vector<Value> values;

   scheduler.scheduleRecurringEvent(bsls::TimeInterval(1.5),
                                  bdlf::BindUtil::bind(&saveData, &values)));
   scheduler.start();
   bsls::TimeInterval start = bsls::SystemTime::nowRealtimeClock();
   while ((bsls::SystemTime::nowRealtimeClock() -
                                         start).totalSecondsAsDouble() < 7) {
     ++g_data;
   }
   scheduler.stop();
   assert(values.size() >= 4);
   for (int i = 0; i < (int) values.size(); ++i) {
       cout << "At " << bdlt::EpochUtil::convertFromTimeInterval(
                                                          values[i].first) <<
               " g_data was " << values[i].second << endl;
   }
This will display, e.g.:
  At 26OCT2020_23:51:51.097283 g_data was 8008406
  At 26OCT2020_23:51:52.597287 g_data was 16723918
  At 26OCT2020_23:51:54.097269 g_data was 24563722
  At 26OCT2020_23:51:55.597262 g_data was 30291748
Example 2: Server Timeouts:
The following example shows how to use a bdlmt::EventScheduler to implement a timeout mechanism in a server. my_Session maintains several connections. It closes a connection if the data for it does not arrive before a timeout (specified at the server creation time).
Because the timeout is relative to the arrival of data, it is best to use a "monotonic" clock that advances at a steady rate, rather than a "wall" clock that may fluctuate to reflect real time adjustments.
    class my_Session{
        // This class encapsulates the data and state associated with a
        // connection and provides a method 'processData' to process the
        // incoming data for the connection.
      public:
        int processData(void *data, int length);
            // Process the specified 'data' of the specified 'length'.  (TBD)
    };

    class my_Server {
     // This class implements a server maintaining several connections.
     // A connection is closed if the data for it does not arrive
     // before a timeout (specified at the server creation time).

     struct Connection {
         bdlmt::EventSchedulerEventHandle d_timerId;   // handle for timeout
                                                     // event

         my_Session *d_session_p;                    // session for this
                                                     // connection
     };

     bsl::vector<Connection*> d_connections; // maintained connections
     bdlmt::EventScheduler    d_scheduler;   // timeout event scheduler
     bsls::TimeInterval       d_ioTimeout;   // time out

     void newConnection(Connection *connection);
         // Add the specified 'connection' to this server and schedule
         // the timeout event that closes this connection if the data
         // for this connection does not arrive before the timeout.

     void closeConnection(Connection *connection);
         // Close the specified 'connection' and remove it from this server.

     void dataAvailable(Connection *connection, void *data, int length);
         // Return if the specified 'connection' has already timed-out.
         // If not, cancel the existing timeout event for the 'connection',
         // process the specified 'data' of the specified 'length' and
         // schedule a new timeout event that closes the 'connection' if
         // the data does not arrive before the timeout.

   public:
     my_Server(const bsls::TimeInterval&  ioTimeout,
               bslma::Allocator         *allocator = 0);
         // Create a 'my_Server' object with a timeout value of
         // 'ioTimeout' seconds.  Optionally specify a 'allocator' used to
         // supply memory.  If 'allocator' is 0, the currently installed
         // default allocator is used.

     ~my_Server();
         // Perform the required clean-up and destroy this object.
 };

 my_Server::my_Server(const bsls::TimeInterval&  ioTimeout,
                      bslma::Allocator          *alloc)
 : d_connections(alloc)
 , d_scheduler(bsls::SystemClockType::e_MONOTONIC, alloc)
 , d_ioTimeout(ioTimeout)
 {
      // TBD: logic to start monitoring the arriving connections or data

      d_scheduler.start();
 }

 my_Server::~my_Server()
 {
     // TBD: logic to clean up

     d_scheduler.stop();
 }

 void my_Server::newConnection(my_Server::Connection *connection)
 {
     // TBD: logic to add 'connection' to 'd_connections'

     // setup the timeout for data arrival
     d_scheduler.scheduleEvent(
        &connection->d_timerId,
        d_scheduler.now() + d_ioTimeout,
        bdlf::BindUtil::bind(&my_Server::closeConnection, this, connection));
 }

 void my_Server::closeConnection(my_Server::Connection *connection)
 {
     // TBD: logic to close the 'connection' and remove it from 'd_ioTimeout'
 }

 void my_Server::dataAvailable(my_Server::Connection *connection,
                               void                  *data,
                               int                    length)
 {
     // If connection has already timed out and closed, simply return.
     if (d_scheduler.cancelEvent(connection->d_timerId)) {
         return;                                                    // RETURN
     }

     // process the data
     connection->d_session_p->processData(data, length);

     // setup the timeout for data arrival
     d_scheduler.scheduleEvent(
        &connection->d_timerId,
        d_scheduler.now() + d_ioTimeout,
        bdlf::BindUtil::bind(&my_Server::closeConnection, this, connection));
 }
Example 3: Using the Test Time Source:
For testing purposes, the class bdlmt::EventSchedulerTestTimeSource is provided to allow a test to manipulate the system-time observed by a bdlmt::EventScheduler in order to control when events are triggered. After a scheduler is constructed, a bdlmt::EventSchedulerTestTimeSource object can be created atop the scheduler. A test can then use the test time-source to advance the scheduler's observed system-time in order to dispatch events in a manner coordinated by the test. Note that a bdlmt::EventSchedulerTestTimeSource must be created on an event-scheduler before any events are scheduled, or the event-scheduler is started.
This example shows how the clock may be altered:
 void myCallbackFunction() {
     puts("Event triggered!");
 }

 void testCase() {
     // Create the scheduler
     bdlmt::EventScheduler scheduler;

     // Create the time-source.
     // Install the time-source in the scheduler.
     bdlmt::EventSchedulerTestTimeSource timeSource(&scheduler);

     // Retrieve the initial time held in the time-source.
     bsls::TimeInterval initialAbsoluteTime = timeSource.now();

     // Schedule a single-run event at a 35s offset.
     scheduler.scheduleEvent(initialAbsoluteTime + 35,
                             bsl::function<void()>(&myCallbackFunction));

     // Schedule a 30s recurring event.
     scheduler.scheduleRecurringEvent(bsls::TimeInterval(30),
                                      bsl::function<void()>(
                                                       &myCallbackFunction));

     // Start the dispatcher thread.
     scheduler.start();

     // Advance the time by 40 seconds so that each
     // event will run once.
     timeSource.advanceTime(bsls::TimeInterval(40));

     // The line "Event triggered!" should now have
     // been printed to the console twice.

     scheduler.stop();
 }
Note that this feature should be used only for testing purposes, never in production code.