Dive into SObjectizer-5.5
SObjectizer Team, Jan 2016
Sixth Part: Synchronous Interaction
(at v.5.5.15)
This is the next part of the series of presentations with deep
introduction into features of SObjectizer-5.5.
This part is dedicated to synchronous interactions between
agents.
SObjectizer Team, Jan 2016
Introduction
SObjectizer Team, Jan 2016
A classical way of interaction between agents is
asynchronous messages.
Asynchronous interaction is simple, flexible and scalable. It
also could prevent some errors like deadlocks (but could
also lead to other kind of errors).
However, sometimes asynchronous interaction makes code
more complex and requires a lot of programmers work...
SObjectizer Team, Jan 2016
For example: sometimes an agent C must send a request to
an agent S and receive back a response with some
important information. The agent C can continue its work
only after arrival of information from agent S.
How we can express that scenario via asynchronous
messages?
SObjectizer Team, Jan 2016
We need a request message which must contain a mbox for
the response (since v.5.5.9 any struct/class which is
MoveConstructible can be used as a message):
struct get_config {
so_5::mbox_t reply_to;
};
We need a response which will be sent back:
struct config {
... // Some fields.
config(...); // Some constructor...
};
SObjectizer Team, Jan 2016
A skeleton for agent S is quite simple:
class config_manager : public so_5::agent_t {
...
public :
virtual void so_define_agent() override {
so_subscribe_self().event( &config_manager::evt_get_config );
... // Other subscriptions...
}
private :
void evt_get_config( const get_config & request ) {
// Sending a reply back.
so_5::send< config >( request.reply_to, ... /* some args */ );
}
};
SObjectizer Team, Jan 2016
But a skeleton of agent C is more complex:
class client : public so_5::agent_t {
// A special state to wait a response.
const so_5::state_t st_wait_config{ this, "wait_config" };
public :
virtual void so_define_agent() override {
// Agent should start its work in a special state.
this >>= st_wait_config;
// Reply with config data will be handled in that state.
st_wait_config.event( &client::evt_config );
...
}
virtual void so_evt_start() override {
// Request a config at start.
so_5::send< get_config >( config_manager_mbox, so_direct_mbox() );
}
private :
void evt_config( const config & cfg ) { ... /* Handling of configuration. */ }
};
SObjectizer Team, Jan 2016
It is a lot of code for such simple operation.
But this code is not robust enough.
For example, there will be no handling of the cases when
get_config request or config response are lost somewhere...
SObjectizer Team, Jan 2016
Synchronous interaction can help here.
An agent C can issue a service request to agent S and
receive the result of that request synchronously.
SObjectizer Team, Jan 2016
Let’s rewrite our small example with service request instead
of asynchronous messages...
SObjectizer Team, Jan 2016
A slight modification for request type. There is no need to
pass reply_to mbox in a request now. So, get_config
becomes a signal:
struct get_config : public so_5::signal_t {};
Response will remain the same:
struct config {
... // Some fields.
config(...); // Some constructors...
};
SObjectizer Team, Jan 2016
Service request processor will return config as a result of
event handler:
class config_manager : public so_5::agent_t {
...
public :
virtual void so_define_agent() override {
so_subscribe_self().event< get_config >( &config_manager::evt_get_config );
... // Other subscriptions...
}
private :
config evt_get_config() {
// Returning a reply back.
return config{ ... /* some args */ };
}
};
SObjectizer Team, Jan 2016
Service request issuer will look like:
class client : public so_5::agent_t {
public :
virtual void so_define_agent() override {
... // No special state, no subscription for config response.
}
virtual void so_evt_start() override {
// Request a config at start.
// Wait response for 1 second.
auto cfg = so_5::request_value< config, get_config >(
config_manager_mbox, std::chrono::seconds(1) );
}
...
};
SObjectizer Team, Jan 2016
That code is not only much simpler ‒ it is also more robust:
● if there is no service request processor behind
config_manager_mbox there will be an exception;
● if there are more than one service request processor behind
config_manager_mbox there will be an exception;
● if config_manager won’t process a request (request is ignored in the
current state of manager) there will be an exception;
● if config_manager can’t process a request, e.g. throw an exception,
that exception will be propagated to service request's issuer;
● if config_manager can’t process a request in the specified time slice
there will be an exception.
SObjectizer Team, Jan 2016
It means that in several cases synchronous interaction via
service requests is more appropriate than asynchronous
interaction.
In such cases service requests allow to write more simple,
clean and robust code…
...but everything has its price.
SObjectizer Team, Jan 2016
The main disadvantage of service request is a possibility of
deadlocks.
Service request’s issuer and processor must work on
different working threads.
It means that issuer and processor must be bound to
different dispatchers. Or, to the different working threads
inside the same dispatcher.
SObjectizer Team, Jan 2016
If an issuer and a processor work on the same working
thread there will be a deadlock.
SObjectizer doesn’t check that. A user is responsible for
binding issuer and processor to the different contexts.
SObjectizer Team, Jan 2016
But the case when an issuer and a processor are working on
the same thread is the simplest case of deadlock.
There could be more complex cases:
● agent A calls agent B;
● agent B calls agent C;
● agent C calls agent D;
● agent D calls agent A.
It is another kind of classical deadlock with the same
consequences.
SObjectizer Team, Jan 2016
Another disadvantage of service request is a blocking of a
working thread for some time.
If a service request issuer shares the working thread with
different agents (for example, all of them are bound to
one_thread dispatcher instance) then all other agents on that
thread will wait until the service request is completed.
It means that synchronous agents interaction is not very
scalable.
SObjectizer Team, Jan 2016
As shown above, the synchronous agents interaction has
significant disadvantages. Based on that, it should be used
with care.
SObjectizer Team, Jan 2016
How Does It Work?
SObjectizer Team, Jan 2016
There is no any magic behind service requests.
Just an ordinary asynchronous messages and some help
from std::promise and std::future...
SObjectizer Team, Jan 2016
When a service request is initiated a special envelope with
service requests params inside is sent as an ordinary
message to service request processor.
This envelope contains not only request params but also a
std::promise object for the response. A value for that
promise is set on processor’s side.
A service request issuer waits on std::future object which is
bound with std::promise from the envelope which was sent.
SObjectizer Team, Jan 2016
It means that so_5::request_value call in form:
auto r = so_5::request_value<Result,Request>(mbox, timeout, params);
It is a shorthand for something like that:
// Envelope will contain Request object and std::promise<Result> object.
auto env__ = std::make_unique<msg_service_request_t<Result,Request>>(params);
// Get the future to wait on it.
std::future<Result> f__ = env__.m_promise.get_future();
// Sending the envelope with request params as async message.
mbox->deliver_message(std::move(env__));
// Waiting and handling the result.
auto wait_result__ = f__.wait_for(timeout);
if(std::future_status::ready != wait_result__)
throw exception_t(...);
auto r = f__.get();
SObjectizer Team, Jan 2016
Every service request handler in the following form
Result event_handler(const Request & svc_req ) { ... }
is automatically transformed to something like this:
void actual_event_handler(msg_service_request_t<Result,Request> & m) {
try {
m.m_promise.set( event_handler(m.query_param()) );
}
catch(...) {
m.set_exception(std::current_exception());
}
}
This transformation is performed during subscription of
event_handler.
SObjectizer Team, Jan 2016
This approach of supporting synchronous interaction means
that service request is handled by SObjectizer just like
dispatching and handling of ordinary message.
As a consequence, a service request handler even doesn’t
know is a message it handles is a part of a service request
or it was sent as a asynchronous message?
Only the service request issuer knows the difference.
SObjectizer Team, Jan 2016
It means that a config_manager from the example above will
work in the same way even if get_config signal is sent via
so_5::send() function like any other async message.
But in this case the return value of config_manager::
evt_get_config will be discarded.
It means that there is no special rules for writing service
request handlers: they are just agents with traditional event
handlers. Except one important aspect...
SObjectizer Team, Jan 2016
This important aspect is exception handling.
If an agent allows an exception to go out from an event
handler then traditional reaction to unhandled exception will
be initiated:
● SObjectizer Environment will call so_exception_reaction() for that
agent (and may be for coop of that agent and so on);
● appropriate action will be taken (for example, an exception could
be ignored or the whole application will be aborted).
SObjectizer Team, Jan 2016
But if an exception is going out from service request handler
then it will be intercepted and returned back to service
request issuer via std::promise/std::future pair.
It means that this exception will be reraised on service
request issuer side during the call to std::future::get()
method. E.g. that exception will be thrown out from
request_value().
SObjectizer Team, Jan 2016
request_value and request_future functions
SObjectizer Team, Jan 2016
There are several ways of initiating service requests.
The simplest one is the usage of so_5::request_value()
template function. It can be used in the form:
Result r = so_5::request_value<Result, Request>(Target, Timeout [,Args]);
Where Timeout is a value which is defined by std::chrono or
so_5::infinite_wait for non-limited waiting of the result.
Type of Request can be any message or signal type. All
Args (if any) will be passed to the constructor of Request.
SObjectizer Team, Jan 2016
Examples of request_value invocations:
// Sending a get_status signal as service request.
auto v = so_5::request_value<engine_status, get_status>(engine,
// Infinite waiting for the response.
so_5::infinite_wait); // No other params because of signal.
// Sending a message convert_value as a service request.
auto v = so_5::request_value<std::string, convert_value>(converter,
// Waiting for 200ms for the response.
std::chrono::milliseconds(200),
"feets", 33000, "metres" ); // Params for the constructor of convert_value.
SObjectizer Team, Jan 2016
There is also so_5::request_future() template function. It
returns std::future<Result> object:
std::future<Result> r = so_5::request_future<Result, Request>(Target [,Args]);
There is no Timeout argument. Waiting on the std::future is
responsibility of a programmer.
As in the case of request_value type of Request can be any
of message or signal type. All Args (if any) will be passed to
the constructor of Request.
SObjectizer Team, Jan 2016
request_future can be used in more complex scenarios than
request_value. For example, it is possible to issue several
service requests, do some actions and only then request
responses:
auto lights = so_5::request_future<light_status, turn_light_on>(light_controller);
auto heating = so_5::request_future<heating_status, turn_heating_on>(heating_controller);
auto accessories = so_5::request_future<accessories_status, update>(accessories_controller);
... // Some other actions.
if(light_status::on != lights.get())
... // Some reaction...
if(heating_status::on != heating.get())
... // Some reaction...
check_accessories(accessories.get());
...
SObjectizer Team, Jan 2016
There could be more complex scenarios:
class accessories_listener : public so_5::agent_t {
public :
...
virtual void so_evt_start() override {
m_status = so_5::request_future< accessories_status, get_status >(accessories_controller());
// Service request initiated, but the response will be used later.
so_5::send_delayed< check_status >(*this, std::chrono::milliseconds(250));
}
private :
std::future<accessories_status> m_status;
...
void evt_check_status() {
auto status = m_status.get(); // Getting the service request response.
... // Processing it.
// Initiate new request again.
m_status = so_5::request_future< accessories_status, get_status >(accessories_controller());
// Response is expected to be ready on the next timer event
so_5::send_delayed< check_status >(*this, std::chrono::milliseconds(250));
}
};
SObjectizer Team, Jan 2016
Service Requests With void As Result Type
SObjectizer Team, Jan 2016
Sometimes there is some sense in initiating a service
request which returns void.
A result of such service request means that processing of a
request is completely finished. Sometimes, is it a very useful
information.
For example, if service request processor does flushing of
some important data, finishing transactions, invalidating
caches and so on...
SObjectizer Team, Jan 2016
An example of service request with void result:
// Type of agent which implements an intermediate buffer with flushing by demand or by timer.
class data_buffer : public so_5::agent_t {
public :
// Signal for flushing the data.
struct flush : public so_5::signal_t {};
...
private :
// Event which is bound to flush signal.
void evt_flush() {
... // Storing the data to the disk/database/cloud...
}
};
// Initiating flush operation as a service request. Return from request_value
// means the completeness of data flushing.
so_5::request_value<void, data_buffer::flush>(buffer, so_5::infinite_wait);
SObjectizer Team, Jan 2016
Additional Information:
Project’s home: http://sourceforge.net/projects/sobjectizer
Documentation: http://sourceforge.net/p/sobjectizer/wiki/
Forum: http://sourceforge.net/p/sobjectizer/discussion/
Google-group: https://groups.google.com/forum/#!forum/sobjectizer
GitHub mirror: https://github.com/masterspline/SObjectizer

Dive into SObjectizer-5.5. Sixth part: Synchronous Interaction

  • 1.
    Dive into SObjectizer-5.5 SObjectizerTeam, Jan 2016 Sixth Part: Synchronous Interaction (at v.5.5.15)
  • 2.
    This is thenext part of the series of presentations with deep introduction into features of SObjectizer-5.5. This part is dedicated to synchronous interactions between agents. SObjectizer Team, Jan 2016
  • 3.
  • 4.
    A classical wayof interaction between agents is asynchronous messages. Asynchronous interaction is simple, flexible and scalable. It also could prevent some errors like deadlocks (but could also lead to other kind of errors). However, sometimes asynchronous interaction makes code more complex and requires a lot of programmers work... SObjectizer Team, Jan 2016
  • 5.
    For example: sometimesan agent C must send a request to an agent S and receive back a response with some important information. The agent C can continue its work only after arrival of information from agent S. How we can express that scenario via asynchronous messages? SObjectizer Team, Jan 2016
  • 6.
    We need arequest message which must contain a mbox for the response (since v.5.5.9 any struct/class which is MoveConstructible can be used as a message): struct get_config { so_5::mbox_t reply_to; }; We need a response which will be sent back: struct config { ... // Some fields. config(...); // Some constructor... }; SObjectizer Team, Jan 2016
  • 7.
    A skeleton foragent S is quite simple: class config_manager : public so_5::agent_t { ... public : virtual void so_define_agent() override { so_subscribe_self().event( &config_manager::evt_get_config ); ... // Other subscriptions... } private : void evt_get_config( const get_config & request ) { // Sending a reply back. so_5::send< config >( request.reply_to, ... /* some args */ ); } }; SObjectizer Team, Jan 2016
  • 8.
    But a skeletonof agent C is more complex: class client : public so_5::agent_t { // A special state to wait a response. const so_5::state_t st_wait_config{ this, "wait_config" }; public : virtual void so_define_agent() override { // Agent should start its work in a special state. this >>= st_wait_config; // Reply with config data will be handled in that state. st_wait_config.event( &client::evt_config ); ... } virtual void so_evt_start() override { // Request a config at start. so_5::send< get_config >( config_manager_mbox, so_direct_mbox() ); } private : void evt_config( const config & cfg ) { ... /* Handling of configuration. */ } }; SObjectizer Team, Jan 2016
  • 9.
    It is alot of code for such simple operation. But this code is not robust enough. For example, there will be no handling of the cases when get_config request or config response are lost somewhere... SObjectizer Team, Jan 2016
  • 10.
    Synchronous interaction canhelp here. An agent C can issue a service request to agent S and receive the result of that request synchronously. SObjectizer Team, Jan 2016
  • 11.
    Let’s rewrite oursmall example with service request instead of asynchronous messages... SObjectizer Team, Jan 2016
  • 12.
    A slight modificationfor request type. There is no need to pass reply_to mbox in a request now. So, get_config becomes a signal: struct get_config : public so_5::signal_t {}; Response will remain the same: struct config { ... // Some fields. config(...); // Some constructors... }; SObjectizer Team, Jan 2016
  • 13.
    Service request processorwill return config as a result of event handler: class config_manager : public so_5::agent_t { ... public : virtual void so_define_agent() override { so_subscribe_self().event< get_config >( &config_manager::evt_get_config ); ... // Other subscriptions... } private : config evt_get_config() { // Returning a reply back. return config{ ... /* some args */ }; } }; SObjectizer Team, Jan 2016
  • 14.
    Service request issuerwill look like: class client : public so_5::agent_t { public : virtual void so_define_agent() override { ... // No special state, no subscription for config response. } virtual void so_evt_start() override { // Request a config at start. // Wait response for 1 second. auto cfg = so_5::request_value< config, get_config >( config_manager_mbox, std::chrono::seconds(1) ); } ... }; SObjectizer Team, Jan 2016
  • 15.
    That code isnot only much simpler ‒ it is also more robust: ● if there is no service request processor behind config_manager_mbox there will be an exception; ● if there are more than one service request processor behind config_manager_mbox there will be an exception; ● if config_manager won’t process a request (request is ignored in the current state of manager) there will be an exception; ● if config_manager can’t process a request, e.g. throw an exception, that exception will be propagated to service request's issuer; ● if config_manager can’t process a request in the specified time slice there will be an exception. SObjectizer Team, Jan 2016
  • 16.
    It means thatin several cases synchronous interaction via service requests is more appropriate than asynchronous interaction. In such cases service requests allow to write more simple, clean and robust code… ...but everything has its price. SObjectizer Team, Jan 2016
  • 17.
    The main disadvantageof service request is a possibility of deadlocks. Service request’s issuer and processor must work on different working threads. It means that issuer and processor must be bound to different dispatchers. Or, to the different working threads inside the same dispatcher. SObjectizer Team, Jan 2016
  • 18.
    If an issuerand a processor work on the same working thread there will be a deadlock. SObjectizer doesn’t check that. A user is responsible for binding issuer and processor to the different contexts. SObjectizer Team, Jan 2016
  • 19.
    But the casewhen an issuer and a processor are working on the same thread is the simplest case of deadlock. There could be more complex cases: ● agent A calls agent B; ● agent B calls agent C; ● agent C calls agent D; ● agent D calls agent A. It is another kind of classical deadlock with the same consequences. SObjectizer Team, Jan 2016
  • 20.
    Another disadvantage ofservice request is a blocking of a working thread for some time. If a service request issuer shares the working thread with different agents (for example, all of them are bound to one_thread dispatcher instance) then all other agents on that thread will wait until the service request is completed. It means that synchronous agents interaction is not very scalable. SObjectizer Team, Jan 2016
  • 21.
    As shown above,the synchronous agents interaction has significant disadvantages. Based on that, it should be used with care. SObjectizer Team, Jan 2016
  • 22.
    How Does ItWork? SObjectizer Team, Jan 2016
  • 23.
    There is noany magic behind service requests. Just an ordinary asynchronous messages and some help from std::promise and std::future... SObjectizer Team, Jan 2016
  • 24.
    When a servicerequest is initiated a special envelope with service requests params inside is sent as an ordinary message to service request processor. This envelope contains not only request params but also a std::promise object for the response. A value for that promise is set on processor’s side. A service request issuer waits on std::future object which is bound with std::promise from the envelope which was sent. SObjectizer Team, Jan 2016
  • 25.
    It means thatso_5::request_value call in form: auto r = so_5::request_value<Result,Request>(mbox, timeout, params); It is a shorthand for something like that: // Envelope will contain Request object and std::promise<Result> object. auto env__ = std::make_unique<msg_service_request_t<Result,Request>>(params); // Get the future to wait on it. std::future<Result> f__ = env__.m_promise.get_future(); // Sending the envelope with request params as async message. mbox->deliver_message(std::move(env__)); // Waiting and handling the result. auto wait_result__ = f__.wait_for(timeout); if(std::future_status::ready != wait_result__) throw exception_t(...); auto r = f__.get(); SObjectizer Team, Jan 2016
  • 26.
    Every service requesthandler in the following form Result event_handler(const Request & svc_req ) { ... } is automatically transformed to something like this: void actual_event_handler(msg_service_request_t<Result,Request> & m) { try { m.m_promise.set( event_handler(m.query_param()) ); } catch(...) { m.set_exception(std::current_exception()); } } This transformation is performed during subscription of event_handler. SObjectizer Team, Jan 2016
  • 27.
    This approach ofsupporting synchronous interaction means that service request is handled by SObjectizer just like dispatching and handling of ordinary message. As a consequence, a service request handler even doesn’t know is a message it handles is a part of a service request or it was sent as a asynchronous message? Only the service request issuer knows the difference. SObjectizer Team, Jan 2016
  • 28.
    It means thata config_manager from the example above will work in the same way even if get_config signal is sent via so_5::send() function like any other async message. But in this case the return value of config_manager:: evt_get_config will be discarded. It means that there is no special rules for writing service request handlers: they are just agents with traditional event handlers. Except one important aspect... SObjectizer Team, Jan 2016
  • 29.
    This important aspectis exception handling. If an agent allows an exception to go out from an event handler then traditional reaction to unhandled exception will be initiated: ● SObjectizer Environment will call so_exception_reaction() for that agent (and may be for coop of that agent and so on); ● appropriate action will be taken (for example, an exception could be ignored or the whole application will be aborted). SObjectizer Team, Jan 2016
  • 30.
    But if anexception is going out from service request handler then it will be intercepted and returned back to service request issuer via std::promise/std::future pair. It means that this exception will be reraised on service request issuer side during the call to std::future::get() method. E.g. that exception will be thrown out from request_value(). SObjectizer Team, Jan 2016
  • 31.
    request_value and request_futurefunctions SObjectizer Team, Jan 2016
  • 32.
    There are severalways of initiating service requests. The simplest one is the usage of so_5::request_value() template function. It can be used in the form: Result r = so_5::request_value<Result, Request>(Target, Timeout [,Args]); Where Timeout is a value which is defined by std::chrono or so_5::infinite_wait for non-limited waiting of the result. Type of Request can be any message or signal type. All Args (if any) will be passed to the constructor of Request. SObjectizer Team, Jan 2016
  • 33.
    Examples of request_valueinvocations: // Sending a get_status signal as service request. auto v = so_5::request_value<engine_status, get_status>(engine, // Infinite waiting for the response. so_5::infinite_wait); // No other params because of signal. // Sending a message convert_value as a service request. auto v = so_5::request_value<std::string, convert_value>(converter, // Waiting for 200ms for the response. std::chrono::milliseconds(200), "feets", 33000, "metres" ); // Params for the constructor of convert_value. SObjectizer Team, Jan 2016
  • 34.
    There is alsoso_5::request_future() template function. It returns std::future<Result> object: std::future<Result> r = so_5::request_future<Result, Request>(Target [,Args]); There is no Timeout argument. Waiting on the std::future is responsibility of a programmer. As in the case of request_value type of Request can be any of message or signal type. All Args (if any) will be passed to the constructor of Request. SObjectizer Team, Jan 2016
  • 35.
    request_future can beused in more complex scenarios than request_value. For example, it is possible to issue several service requests, do some actions and only then request responses: auto lights = so_5::request_future<light_status, turn_light_on>(light_controller); auto heating = so_5::request_future<heating_status, turn_heating_on>(heating_controller); auto accessories = so_5::request_future<accessories_status, update>(accessories_controller); ... // Some other actions. if(light_status::on != lights.get()) ... // Some reaction... if(heating_status::on != heating.get()) ... // Some reaction... check_accessories(accessories.get()); ... SObjectizer Team, Jan 2016
  • 36.
    There could bemore complex scenarios: class accessories_listener : public so_5::agent_t { public : ... virtual void so_evt_start() override { m_status = so_5::request_future< accessories_status, get_status >(accessories_controller()); // Service request initiated, but the response will be used later. so_5::send_delayed< check_status >(*this, std::chrono::milliseconds(250)); } private : std::future<accessories_status> m_status; ... void evt_check_status() { auto status = m_status.get(); // Getting the service request response. ... // Processing it. // Initiate new request again. m_status = so_5::request_future< accessories_status, get_status >(accessories_controller()); // Response is expected to be ready on the next timer event so_5::send_delayed< check_status >(*this, std::chrono::milliseconds(250)); } }; SObjectizer Team, Jan 2016
  • 37.
    Service Requests Withvoid As Result Type SObjectizer Team, Jan 2016
  • 38.
    Sometimes there issome sense in initiating a service request which returns void. A result of such service request means that processing of a request is completely finished. Sometimes, is it a very useful information. For example, if service request processor does flushing of some important data, finishing transactions, invalidating caches and so on... SObjectizer Team, Jan 2016
  • 39.
    An example ofservice request with void result: // Type of agent which implements an intermediate buffer with flushing by demand or by timer. class data_buffer : public so_5::agent_t { public : // Signal for flushing the data. struct flush : public so_5::signal_t {}; ... private : // Event which is bound to flush signal. void evt_flush() { ... // Storing the data to the disk/database/cloud... } }; // Initiating flush operation as a service request. Return from request_value // means the completeness of data flushing. so_5::request_value<void, data_buffer::flush>(buffer, so_5::infinite_wait); SObjectizer Team, Jan 2016
  • 40.
    Additional Information: Project’s home:http://sourceforge.net/projects/sobjectizer Documentation: http://sourceforge.net/p/sobjectizer/wiki/ Forum: http://sourceforge.net/p/sobjectizer/discussion/ Google-group: https://groups.google.com/forum/#!forum/sobjectizer GitHub mirror: https://github.com/masterspline/SObjectizer