SObjectizer  5.8
Loading...
Searching...
No Matches
so-5.5 In Depth - Synchronous Interaction

Introduction

A major change was introduced in v.5.3.0 – a possibility to synchronously wait for result of message processing. An agent, called service client, could ask another agent (called service_handler) to do something by asynchronously sending message to service_handler and then synchronously waiting for the result of that request.

All parameters for service request must be defined as an ordinary message or a signal type:

// Message for service request with parameters.
struct msg_convert
{
// String to conversion to int.
std::string m_value;
};
// Signal for service request without parameters.
struct msg_get_status : public so_5::rt::signal_t {}

Service request handler is an ordinary agent which defines its service_request handling methods by traditional way. The only change with previous version is that event handlers could return values since v.5.3.0:

// service_handler event for msg_convert request.
int a_my_service_t::evt_convert( const msg_convert & msg )
{
std::istringstream s( msg.m_value );
int result;
s >> result;
if( s.fail() )
throw std::invalid_argument( "unable to convert to int: '" + msg.m_value + "'" );
return result;
}
// service_handler event for msg_get_status request.
std::string a_my_service_t::evt_get_status()
{
return "Ready";
}

Event handler methods are subscribed as usual:

class a_my_service_t : public so_5::rt::agent_t
{
public :
virtual void so_define_agent() override
{
so_subscribe( mbox ).event( &a_my_service_t::evt_convert );
so_subscribe( mbox ).event< msg_get_status >( &a_my_service_t::evt_get_status );
}
...
};

All of these mean that there is almost no differences for service_handler: it always works as an ordinary agent. And all event handlers for that agent are invoked on agent’s working thread.

All requests are delivered to service_handler’s agent as ordinary messages via agent’s event queue. The mechanism for event handler invocation for service requests is the same as for traditional asynchronous messages.

To send service request it is necessary to call so_5::request_value template function:

std::string s = so_5::request_value<std::string, msg_get_status>(processor, timeout);

This call will send msg_get_status signal to the target processor and will wait for the result for timeout.

A mbox, a reference to an agent, an ad-hoc agent proxy could be used as target argument for so_5::request_value() function. A caller must ensure that there is only one receiver of msg_get_status behind that argument.

The timeout argument is very important.

The most simple and the most dangerous way, is to wait service request result infinitely long:

// Initiate the service request and wait for result infinitely.
int v = so_5::request_value<int, msg_convert>(mbox, so_5::infinite_wait, "42");
// Invocation of service request by a signal.
std::string s = so_5::request_value<std::string, msg_get_status>(processor, so_5::infinite_wait);
const infinite_wait_indication infinite_wait
A special indicator for infinite waiting on service request or on receive from mchain.

The second way, which is much more safer, is to wait service request result for the specified amount of time:

// Initiate the service request and wait for 300ms.
int v = so_5::request_value<int, msg_convert>(mbox, std::chrono::milliseconds(300), "42");
// Invocation of service request by a signal and wait for 5s.
std::string s = so_5::request_value<std::string, msg_get_status>(processor, std::chrono::seconds(5));

There is also template function so_5::request_future() which provides the most complex and the most flexible way for issuing service request. It returns std::future object associated with service request. A user then can perform any operation he wish:

std::future< int > v = so_5::request_future<int, msg_convert>(mbox, "42");
std::future< std::string > s = so_5::request_future<std::string, msg_get_status>(mbox);
// Do some useful stuff until conversion result get ready.
while( true )
{
... // Do something here.
// Check the future status.
if( std::future_status::ready == v.wait_for( std::chrono::seconds(0) ) )
break;
}
// Conversion result is ready and could be obtained without waiting.
std::cout << "Conversion result: " << v.get() << std::endl;
// Wait for get_status request infinitely.
std::cout << "Service status: " << s.get() << std::endl;

How Does It Work?

When a service request is initiated a special envelope with service requests params inside is sent as 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 sent.

It means that so_5::request_value call in form:

auto r = so_5::request_value<Result,Request>(mbox, timeout, params);

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();

Every service request handler in 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.

Deadlocks

There is no any help to user from SObjectizer to avoid or to diagnose deadlocks when synchronous interactions are used. It is the user task to guarantee that service_handler and service client are working on different threads and there is no any complex internal interactions like: agent A1 calls A2 which calls A3 which calls A1.

It is the reason why the usage of so_5::infinite_wait is dangerous. If there is a deadlock the application will hang forever.

The safest way – is to use so_5::request_value with the reasonable timeout. Even if a deadlock is here it will be automatically destroyed after timeout expiration.

Exceptions

There is a huge possibility of exception appearance during synchronous interactions.

Some exceptions could be raised by SObjectizer. Those are:

  • the exceptions related to wrong number of actual service_handlers. When a service request is initiated by request_value/request_future function the one and only one service_handler must exist (e.g. one and only one agent must be subscribed to the specified message type from the specified mbox). If there are more than one agent-subscriber or if there isn’t any of them then an exception will be raised;
  • the exceptions related to ignorance of service request. For example if service_handler receives a service request but not handled it because the event handler is disabled in the current agent state then an exception will be raised;
  • the exceptions related to timeouts during wait for result. Those exceptions are raised when so_5::request_value() function is used and timeout elapsed without receiving a result of service request.

In such cases SObjectizer raises object of so_5::exception_t class with the appropriate error_code inside.

Some exceptions could be raised by event handlers itself. As in the sample of evt_convert event above. Those exceptions are intercepted by SObjectizer and then transferred to the std::future connected with the service request. It means that those exceptions will be reraised during std::future::get() invocation. So, the application exception which are went out from event handler will be propagated to the service client.

The dialing with non-handled application exception is the main difference between ordinary event-handling and service_request-handling. When agent lets the exception go out during ordinary event handler invocation then SObjectizer calls agent’s so_exception_reaction() method and does appropriate actions. But when agent lets the exception go out during service_request handling then the exception is propagated to service client and so_exception_reaction() is not called.