Проблема и ее актуальность
Возможность синхронного взаимодействия между агентами в SObjectizer была добавлена в версии 5.3.0 летом 2014-го. В это время в SObjectizer-е еще не было ни семейства send-функций, ни возможности отсылки сообщений произвольных типов, ни mchain-ов. Кроме того, при реализации механизма синхронного взаимодействия не был должным образом учтен опыт других акторных фреймворков (в частности Akka). Поэтому первая реализация синхронного взаимодействия получилась громоздкой и неудобной.
Со временем SObjectizer перестал ориентироваться на компиляторы без поддержки variadic templates, что позволило добавить функции request_value и request_future для упрощения синхронного взаимодействия. Использовать синхронное взаимодействие стало проще. Но при этом внутри SObjectizer-5.5 все равно был тот же самый механизм доставки синхронных сообщений, реализованный в версии 5.3.0.
За прошедшее время сложилось устойчивое ощущение, что существовавший в версиях 5.3-5.5 механизм синхронного взаимодействия должен быть упразднен и версии 5.6 он должен быть заменен чем-то новым.
К существующему механизму есть ряд претензий:
1. Обработка синхронных запросов ведет к дополнительным накладным расходам, которые есть всегда, даже когда синхронные запросы не используются. Это и дополнительные проверки в mbox-ах при доставке сообщения, это и дополнительные проверки в специальных обертках, которые SObjectizer создает для каждого обработчика сообщений при подписке.
2. У разработчика нет простого способа делегировать обработку синхронного запроса другому агенту. Так, если в случае обычных сообщений можно сделать агент-балансировщик, который получает сообщение, а затем пересылает сообщение для обработки какому-то другому агенту, то в случае с синхронными запросами это сделать нельзя.
3. Реакция на исключения, которые покинули обработчик сообщения агента, принципиально отличается для обычного сообщения и для синхронного запроса. Так, если агент подписался на сообщение типа M и при его обработке выпустил наружу исключение, то SObjectizer по умолчанию завершит работу всего приложения. Но если M пришло к этому же самому агенту как синхронный запрос, то исключение будет поймано и возвращено как результат синхронного запроса.
4. Агент-обработчик не знает, вызывается ли его обработчик события в результате доставки обычного сообщения или синхронного запроса. Более того, агент не может указать SObjectizer-у что какой-то из его обработчиков должен быть использован только для обработки синхронных запросов. Что может вести к потере важных возвращаемых агентом значений в случае, если агенту-сервису отослали обычное сообщение, а не синхронный запрос.
5. Механизм синхронного взаимодействия из версий 5.3-5.5 существенно усложняет разработку собственных mbox-ов.
Предложение по исправлению проблемы
После того, как в ветке 5.5 появилась поддержка mchain-ов открылась возможность сделать другую реализации синхронного взаимодействия между агентами. Но ее внедрение поломало бы совместимость, поэтому пока ветка 5.5 развивалась, механизм синхронного взаимодействия не менялся.
Однако, поскольку ветка 5.6 нарушает совместимость с веткой 5.5, то есть возможность удалить из SObjectizer старый механизм синхронного взаимодействия и внедрить новый.
Новый механизм синхронного взаимодействия
Суть нового механизма взаимодействия состоит в следующем:
1. Да синхронного взаимодействия агент-клиент отсылает агенту-сервису специальное сообщение, например, имеющее следующий тип:
template<typename Request, typename Reply>
class request_reply_t final : public so_5::message_t {
public:
...
const Request & request() const noexcept;
...
template<typename... Args>
void reply(Args && ...args);
};
Отсылка запроса и получение результата может происходить посредством вспомогательной функции request_value() по аналогии с тем, как это происходит сейчас.
2. Агент-сервис подписывается на сообщение request_reply_t<Req,Rep>, где в качестве Req и Rep будут использоваться конкретные типы данных. Например:
class service_provider_t : public so_5::agent_t {
void on_request(mutable_mhood_t<request_reply_t<SomeRequest, SomeResponse>> cmd);
...
void so_define_agent() override {
so_subscribe_self().event(&service_provider_t::on_request);
...
}
};
3. Агент-сервис отвечает на запрос вызывая метод reply() у полученного экземпляра request_reply_t:
void service_provider_t::on_request(
mutable_mhood_t<request_reply_t<SomeRequest, SomeResponse>> cmd) {
... // Какая-то обработка.
// Отсылка ответа.
cmd->reply(... /* параметры для ответа */)
}
4. Под капотом этого механизма будет использоваться mchain. Когда вызывается request_value (или аналогичная функция), то создается уникальный mchain, ссылка на который передается в экземпляр request_reply_t. Когда агент-сервис вызывает request_reply_t::reply() в этот mchain записывается ответное сообщение. Соответственно, агент-клиент может ждать на этом mchain-е посредством обычных функций receive() или select().
Принцип работы нового механизма в двух словах
Предлагаемый механизм должен работать следующим образом:
* когда агент-клиент вызывает request_value() или другую аналогичную функцию, то создается специальный mchain для получения ответа от агента-сервиса и экземпляр сообщения request_reply_t (куда отдается ссылка на mchain);
* сообщение request_reply_t доставляется до агента-сервиса обычным образом (т.е. SObjectizer внутри не будет разбираться с тем, пришло ли обычное асинхронное сообщение или же синхронный запрос);
* агент-сервис должен записать ответ в ранее созданный mchain через вызов метода request_reply_t::reply();
* агент-клиент ожидает ответа на mchain-е (это ожидание может быть скрыто от агента-клиента функциями вроде request_value()).
При этом, вероятно, сообщение request_reply_t должно отсылаться как мутабельное сообщение.
Достоинства предлагаемого способа
Предполагается, что новый способ организации синхронного взаимодействия будет лишен недостатков старого механизма из версий 5.3-5.5, а именно:
* накладные расходы будут присутствовать только для отсылки request_reply_t, не будет никаких дополнительных проверок в обертках вокруг обработчиков сообщений;
* в реализациях mbox-ов не потребуется больше выполнять доставку обычных сообщений и синхронных запросов разными способами;
* агент-сервис явным образом декларирует, что он выполняет обработку синхронных запросов;
* сообщения request_reply_t можно переадресовывать как и любые другие сообщения;
* единообразный подход к обработке исключений, выпущенных из обработчиков событий агента-сервиса;
* возможность использования select() для ожидания и обработки ответов сразу от нескольких агентов-сервисов.
Степень проработанности нового способа
На данный момент новый способ реализации синхронного взаимодействия в деталях не проработал. Есть лишь вышеописанные верхнеуровневые соображения, которые выглядят вполне реализуемыми на практике.
Детальная проработка нового решения начнется, предположительно, после 10 марта 2019-го.
Зачем этот вопрос вынесен на обсуждение?
Обсуждение замены механизма синхронного взаимодействия преследует две цели:
1. Выяснение того, будет ли для кого-то из пользователей SObjectizer-а смена механизма синхронного взаимодействия агентов критичной? Лично я сильно сомневаюсь, что сейчас синхронное взаимодействие используется широко. Но если где-то оно используется активно, то хотелось бы об этом узнать, чтобы понять, насколько просто или непросто будет переходить на новый способ.
2. Выяснение того, можно ли сделать новый способ взаимодействия еще удобнее, гибче и мощнее.
Соответственно, просьба к заинтересованным читателям: если вы используете механизм синхронного взаимодействия сейчас и видите, что новый механизм усложнит вам переход на версию 5.6, то дайте нам знать. Будем искать способы упростить этот переход.
Ну и если кто-то видит косяки и недостатки предлагаемого способа, то озвучьте свое мнение, пожалуйста. Это поможет сделать SObjectizer-5.6 лучше.