-
Notifications
You must be signed in to change notification settings - Fork 46
SO 5.7 InDepth Timers
Timers are actively used in typical SObjectizer’s based applications.
That's why SObjectizer provides easy to use tools for dealing with timers:
- delayed messages;
- periodic messages.
A delayed message is delivered after a specified time interval since the message was sent. It means that if a delayed message is sent at some time point T and the delay is 150ms then it will be pushed to subscribers’ event queues at the time point (T+150ms).
A periodic message is repeatedly delivered after a specified amount of time. It means that if a periodic message is sent at some time point T with the delay of 150ms and repetition period of 250ms then it will be pushed to subscribers’ event queues first time at the time point (T+150ms), then at the time point (T+400ms), then at (T+650ms) and so on.
A periodic message will be repeated until it canceled.
There is just one way of sending delayed messages in SObjectizer-5.7. It is the send_delayed()
function.
This is an example of sending a delayed message from ordinary agent:
class my_agent : public so_5::agent_t {
...
void so_evt_start() override {
so_5::send_delayed< some_message >(
dest, // Destination mbox.
std::chrono::milliseconds(125), // Delivery delay in ms.
... ); // Arguments to be forwarded to some_message constructor.
}
};
This is an example of sending a delayed message to the direct mbox of the agent-receiver:
class my_agent : public so_5::agent_t {
...
void evt_request( const request & evt ) {
initiate_request_processing( evt );
so_5::send_delayed< check_request >(
*this, // Destination for message.
std::chrono::milliseconds(125), // Delivery delay in ms.
... ); // Arguments to be forwarded to check_request constructor.
}
};
Note that send_delayed()
function returns void
. It means that if a user wants to cancel a delayed message he/she has to use send_periodic()
message with period
param set to zero. See below for more details.
A periodic message is repeated again and again until it will be canceled.
The same message instance is delivered every time. It means that message instance is not deallocated after processing. Deallocation will occur when the message will be canceled.
There just one way of sending a periodic message in SObjectizer-5.7. It is the send_periodic()
function.
This is an example of sending of a periodic message from an agent:
class my_agent : public so_5::agent_t {
so_5::timer_id_t m_status_timer;
...
void so_evt_start() override {
m_status_timer = so_5::send_periodic< update_status >(
dest, // Destination mbox.
std::chrono::milliseconds(125), // First delivery delay in ms.
std::chrono::milliseconds(250), // Repetition period in ms.
... ); // Arguments to be forwarded to update_status constructor.
}
};
Function send_periodic
can also be used for sending a periodic message to the direct mbox of an agent:
class my_agent : public so_5::agent_t {
so_5::timer_id_t m_status_timer;
...
void so_evt_start() override {
m_status_timer = so_5::send_periodic< update_status >(
*this, // Destination.
std::chrono::milliseconds(125), // First delivery delay in ms.
std::chrono::milliseconds(250), // Repetition period in ms.
... ); // Arguments to be forwarded to update_status constructor.
}
};
The most important moment in periodic messages sending ‒ is storing the result value of send_periodic()
.
If the result value is not saved then the periodic message will be canceled immediately. This is because the destructor of timer_id_t
does timer cancellation.
The so_5::timer_id_t
class works like a smart pointer. Destruction of the last timer_id_t
pointed to a timer will destroy the timer and periodic (or delayed) message will be canceled.
That’s why at least one timer_id_t
object for the periodic message must exist while message delivery is necessary.
Sometimes it is necessary to send a delayed message that should be cancelable. To cancel the delivery of a delayed/periodic message it is necessary to have a timer_id_t
for that message. Only send_periodic()
function returns that ID, send_delayed()
function returns void
. Because of that send_periodic()
should be used for sending cancelable delayed messages. In that case, a value of period
parameter should be zero. For example:
class cancelable_delayed_message_demo final : public so_5::agent_t {
so_5::timer_id_t m_delayed_id;
...
void on_some_event(mhood_t<some_event> cmd) {
// Send cancelable delayed message by using send_periodic.
m_delayed_id = so_5::send_periodic<some_msg>(
dest, // The destination.
std::chrono::seconds{10}, // Delay value.
std::chrono::seconds::zero(), // Zero as period value.
...);
...
}
};
There are three ways of delayed/periodic messages cancellation.
All of them use timer_id_t
objects. It means that cancellation is only possible for messages sent via send_periodic()
.
The first way is to call release()
method of timer_id_t
class.
auto id = so_5::send_periodic< Msg >(...);
...
id.release(); // Delivery canceled.
Please note that the explicit call of release()
method cancels a message regardless of the count of remaining timer_id_t
objects pointed to that timer.
The second way is the destruction of all timer_id_t
objects pointing to the same timer.
If release()
method is not called explicitly it will be called in the destructor of the last timer_id_t
object pointing to a timer. This way is often used in agents:
class request_processor : public so_5::agent_t {
so_5::timer_id_t m_check_request;
...
void evt_request( const request & evt ) {
m_check_request = so_5::send_periodic< check_request >(
*this, ...); // Timer will be canceled automatically in
// the destructor of request_processor.
...
}
};
The third way is the assignment of new value to timer_id_t
object.
If this object was the last timer_id_t
pointed to a timer then the timer will be destroyed and message will be cancelled:
auto id = so_5::send_periodic< Msg >(...);
... // Some actions.
id = so_5::send_periodic< Sig >(...); // Cancelation of Msg.
There is a tricky moment with the cancelation of delayed messages...
A delayed message will be canceled only if it is still under control of timer thread. If message already left timer thread and is waiting in event queues of recipients then message delivery will not be canceled and message will be processed by subscribers.
For example, if the delay was 125ms and cancelation is initiated after 125ms after the call to send_delayed()
there is a high probability that message will be delivered anyway.
There is so5extra project that contains the implementation of revocable timers. That implementation guarantees that canceled message will be revoked even if it left the timer thread and is waiting in the event queue of the subscriber.
SO Environment starts a special thread for handling timers. This thread is known as timer thread.
All timers are controlled and processed by that timer thread.
Timer thread can efficiently process a big amount of timers: tens and hundreds of millions. Even billions of timers.
A user can choose a timer mechanism most appropriate for application needs.
Three timer mechanisms are supported. Each has its strengths and weakness:
- timer_wheel
- timer_list
- timer_heap
Can support a very big amount of timers efficiently (tens, hundreds of millions, billions). It is also equally efficient for delayed and periodic messages.
Because of that timer_wheel mechanism should be used when the application needs a big number of timers.
But there are some costs:
- this mechanism is not very precise (there is a step of timer wheel which could be configured, but small step decrease effectiveness);
- this mechanism consumes some resources even if there are no ready to use timers (this overhead is small but it is still here).
Works very well only if new timers will be added to the end of the list of timers. Therefore this mechanism should be used in applications where there are many similar delayed messages with the same delays.
This mechanism does not consume resources when there are no ready to use timers. It also handles timers cancelation very efficiently.
Has very fluent overall performance, especially on relatively small amounts of timers (thousands, tens of thousands timers). It also does not consume resources if there are no ready to use timers.
Because of that timer_heap mechanism is used in SO Environment by default.
Timer mechanism can be specified in Environment’s parameters before start of SO Environment:
so_5::launch( []( so_5::environment_t & env ) {
// Some initialization stuff...
},
// SObjectizer Environment parameters tuning.
[]( so_5::environment_params_t & params ) {
// Use timer_wheel mechanism with wheel size 10000
// and timer step size of 5ms.
params.timer_thread( so_5::timer_wheel_factory(10000, std::chrono::milliseconds(5)) );
...
} );
For more information about timer mechanisms, their strengths and weakness see description of Timer Template Thread (timertt) library. This library is used for implementation of delayed and periodic messages in SO-5.5, SO-5.6, SO-5.7.