Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Dive into SObjectizer-5.5. Sixth part: Synchronous Interaction


Published on

Next part of serie with deep dive into features of SObjectizer-5.5. A brief explanation of synchronous interaction between agents.

Published in: Software
  • Be the first to comment

Dive into SObjectizer-5.5. Sixth part: Synchronous Interaction

  1. 1. Dive into SObjectizer-5.5 SObjectizer Team, Jan 2016 Sixth Part: Synchronous Interaction (at v.5.5.15)
  2. 2. 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
  3. 3. Introduction SObjectizer Team, Jan 2016
  4. 4. 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
  5. 5. 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
  6. 6. 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
  7. 7. 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
  8. 8. 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
  9. 9. 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
  10. 10. 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
  11. 11. Let’s rewrite our small example with service request instead of asynchronous messages... SObjectizer Team, Jan 2016
  12. 12. 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
  13. 13. 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
  14. 14. 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
  15. 15. 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
  16. 16. 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
  17. 17. 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
  18. 18. 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
  19. 19. 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
  20. 20. 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
  21. 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. 22. How Does It Work? SObjectizer Team, Jan 2016
  23. 23. 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
  24. 24. 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
  25. 25. 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
  26. 26. 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
  27. 27. 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
  28. 28. 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
  29. 29. 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
  30. 30. 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
  31. 31. request_value and request_future functions SObjectizer Team, Jan 2016
  32. 32. 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
  33. 33. 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
  34. 34. 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
  35. 35. 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
  36. 36. 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
  37. 37. Service Requests With void As Result Type SObjectizer Team, Jan 2016
  38. 38. 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
  39. 39. 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
  40. 40. Additional Information: Project’s home: Documentation: Forum: Google-group:!forum/sobjectizer GitHub mirror: