Недавно была зафиксирована версия 5.8.3 для SObjectizer и версия 1.6.2 для сопутствующего ему проекта so5extra. В данной статье попробую рассказать о том, что и зачем появилось в новых версиях.
Для тех же, кто про SObjectizer слышит впервые, очень кратко: это относительно небольшой C++17 фреймворк, который позволяет использовать в С++ программах такие подходы, как Actor Model, Publish-Subscribe и Communicating Sequential Processes (CSP). Основная идея, лежащая в основе SObjectizer, — это построение приложения из мелких сущностей-агентов, которые взаимодействуют между собой через асинхронный обмен сообщениями. Составить впечатление о SObjectizer-е и о so5extra можно вот по этим статьям: SObjectizer: что это, для чего это и почему это выглядит именно так? Взгляд из 2022-го и Краткий обзор библиотеки so5extra с дополнениями для SObjectizer-5.
Если же вы уже наслышаны про SObjectizer, то давайте взглянем на обновления. В первой части быстренько пробежимся по нововведениям в самом SObjectizer, а затем рассмотрим новинку из so5extra, которая позволяет подписываться на базовый тип сообщения и получать затем сообщения производных типов.
Новое в SObjectizer-5.8.3
inspect_msg, receives и wait_event_handler_completion в experimental::testing
Когда-то давно в SObjectizer были добавлены средства для написания юнит-тестов для агентов. Размещены эти инструменты были в пространстве имен so_5::experimental::testing
. Имя experimental там было неспроста -- это была самая первая попытка, а первый блин, как известно, комом. Так что хотелось сперва посмотреть на то, что из этого выйдет и лишь затем, с учетом опыта, переводить наработки в разряд стабильных. Но прошло уже без малого шесть лет, а юнит-тестирование для агентов по-прежнему остается в статусе "экспериментального". Как-то опыт накапливается совсем уж неторопливо :(
Однако, пусть и медленно, но процесс идет, иногда пользователи сообщают нам о том, чего им не хватает, а мы пытаемся с этим разобраться.
Так была добавлена новая функция inspect_msg
, которая позволяет проанализировать содержимое сообщения и сохранить результат в соответствующем шаге тестового сценария, чтобы затем проверить этот результат:
namespace tests = so_5::experimental::testing;
tests::testing_env_t sobj;
...
// Определяем шаг тестового сценария, в котором нам нужна инспекция.
sobj.scenario().define_step("step-with-inspection")
.when(*some_agent
// Убеждаемся, что агент реагирует на сообщение.
& tests::reacts_to<some_message>()
// Проверяем содержимое входящего сообщения и сохраняем результат
// с тегом `inspection-result` для этого шага сценария.
& tests::inspect_msg("inspection-result",
// Эта лямбда будет вызвана для проверки содержимого сообщения.
[](const some_message & msg) -> std::string {
if(msg.field_one != expected_value) return "field_one mismatch";
if(msg.field_two != another_expected_value) return "field_two mismatch";
... // Какие-то другие проверки.
return "OK"; // Раз добрались сюда, значит все OK.
})
);
...
sobj.scenario().run_for(100ms);
REQUIRE(tests::complected() == sobj.scenario().result());
// Поскольку сценарий выполнился, то можно проверить что получилось.
// Извлекаем сохраненный результат проверки содержимого сообщения и
// сравниваем с тем, что ожидалось.
REQUIRE("OK" == sobj.scenario().stored_msg_inspection_result(
"step-with-inspection", "inspection-result"));
...
Еще бывает нужно удостовериться что некое сообщение было отправлено в конкретный mbox. Не агенту (поскольку это проверяется уже существующим триггером reacts_to
), а именно в mbox. Поэтому добавлен новый триггер receives
, который срабатывает когда сообщение отсылается в указанный mbox:
namespace tests = so_5::experimental::testing;
tests::testing_env_t sobj;
...
// Mbox в котором мы будем ждать появление сообщения.
const so_5::mbox_t dest = sobj.environment().create_mbox();
...
// Проверяем, что агент среагировал на входящее сообщение.
sobj.scenario().define_step("agent-reacts-to-initial-msg")
.impact<initial_msg>(*agent_to_check, ...)
.when(*agent_to_check
// Убеждаемся, что агент реагирует на сообщение.
// В обработчике новое сообщение должно быть отослано в dest.
& tests::reacts_to<initial_msg>());
// Проверяем, что сообщение прилетело в нужный нам mbox.
sobj.scenario().define_step("expected-msg-sent")
.when(dest & tests::receives<expected_msg>());
...
sobj.scenario().run_for(100ms);
REQUIRE(tests::complected() == sobj.scenario().result());
Кстати говоря, receives
можно комбинировать с inspect_msg
, т.е. можно не только проверить факт попадания сообщения в mbox, но и убедиться, что сообщение имеет нужное содержимое.
Важное примечание: inspect_msg
не может использоваться для сигналов, т.к. сигналы не переносят внутри себя никаких данных, соответственно, инспектировать нечего.
И еще одна новая штука, которая может быть полезна в некоторых специфических ситуациях -- это wait_event_handler_completion
.
Тут суть в том, что у каждого шага сценария есть несколько состояний: не активирован, преактивирован, активирован и завершен. Для того, чтобы тестовый сценарий закончился нужно, чтобы все шаги сценария были завершены. Но фокус в том, что простые шаги сценария завершаются сразу же после своей активации:
sobj.scenario().define_step("fast-one")
.impact<initial_msg>(*agent_to_check)
.when(*agent_to_check & tests::reacts_to<initial_msg>());
Вот этот шаг будет активирован как только к агенту прилетит сообщение initial_msg
. Но т.к. больше никаких операций для этого шага не описано, то шаг fast-one автоматически завершится сразу же после активации. Даже без ожидания окончания обработки сообщения initial_msg
агентом agent_to_check
.
А теперь представим, что agent_to_check
в своем событии модифицирует какие-то разделяемые данные, состояние которых мы хотим проверить после выполнения сценария. Т.е. пусть в грубой форме у нас есть что-то вроде:
std::string value_receiver;
auto * agent_to_check = sobj.environment().introduce_coop(
[&value_receiver](so_5::coop_t & coop) {
return coop.make_agent<some_agent_type>(
// Ссылка на value_receiver уходит агенту.
value_receiver,
...);
});
... // Определяем какие-то шаги сценария.
// Самый последний шаг, в котором мы проверяем, что агент
// реагирует на входящее сообщение.
// Именно при обработке данного сообщения агент
// модифицирует value_receiver.
sobj.scenario().define_step("last-one")
.when(*agent_to_check & tests::reacts_to<some_msg>());
// Запускаем сценарий.
sobj.scenario().run_for(100ms);
// Убеждаемся, что сценарий отработал успешно...
REQUIRE(tests::complected() == sobj.scenario().result());
// ...после чего проверяем нужные нам значения.
REQUIRE(expected_value == value_receiver);
Проблема здесь в том, что шаг last-one будет считаться завершенным сразу после активации, не дожидаясь окончания работы event-handler-а для some_msg
. А это значит, что и весь сценарий закончится еще до того, как этот event-handler выполнится. А это значит, что мы можем начать сравнивать значение value_receiver
пока на другой нити агент его модифицирует.
Данная проблема может показаться надуманной, ведь агенты не должны делить свое состояние с кем-либо. Но давайте представим, что вместо std::string
у нас в качестве value_receiver
какой-то mock-объект у которого текстируемый агент должен вызвать несколько методов, а мы затем проверяем, что именно эти методы и были вызваны. Тогда ситуация станет гораздо реалистичнее.
Чтобы решить эту проблему как раз и добавлен модификатор wait_event_handler_completion
:
// Самый последний шаг, в котором мы проверяем, что агент
// реагирует на входящее сообщение.
// При этом именно при обработке данного сообщения агент
// модифицирует value_receiver.
sobj.scenario().define_step("last-one")
.when(*agent_to_check
& tests::reacts_to<some_msg>()
// Заставляем сценарий ждать пока агент завершит обработку
// своего входящего сообщения.
& tests::wait_event_handler_completion());
В этом случае шаг last-one активируется когда сообщение some_msg
прилетает к агенту, но завершается только когда обработка some_msg
заканчивается. Поэтому и весь тестовый сценарий будет считать законченным только когда agent_to_check
разберется с some_msg
, но не раньше.
Трассировка отдельных сообщений
В SObjectizer есть возможность трассировки процесса доставки сообщений до получателя. Но его неудобно использовать в больших приложениях, где летают сотни тысяч сообщений, т.к. трассируется вообще все. В принципе, можно написать отсекающий лишнее фильтр, но... Это же нужно писать, что неудобно.
В SO-5.8.3 добавлена возможность "заказать" трассировку только для отдельных сообщений/сигналов.
Для этого нужно сперва включить трассировку и установить специальный фильтр:
so_5::launch([](so_5::environment_t & env) {...},
[](so_5::environment_params_t & params) {
// Включаем трассировку с выводом на std::cout.
params.message_delivery_tracer(
so_5::msg_tracing::std_cout_tracer());
// Устанавливаем специальный фильтр, без которого индивидуальная
// трассировка не будет работать.
params.message_delivery_tracer_filter(
so_5::msg_tracing::make_individual_trace_filter());
});
А затем нужно отсылать сообщения следующим образом:
so_5::send<MsgToTrace>(so_5::msg_tracing::individual_trace(dest), ...);
Т.е. вместо того, чтобы отсылать сообщение MsgToTrace
напрямую в dest
, мы создаем вокруг dest
специальную обертку, которая взаимодействует с упомянутым выше трассировочным фильтром и разрешает печать трассировочных уведомлений именно для этого экземпляра.
В общем, предполагается следующее использование individual_trace
:
вы у себя в коде отправляете сообщение, которое до получателя почему-то не доходит, а вы не понимаете почему;
тогда вы временно включаете трассировку сообщений и заменяете свой send на send с использованием
individual_trace
;разбираетесь с проблемой;
возвращаете в коде нормальный send.
Т.е. здесь изначально предполагается, что вы на какой-то время заменяете нормальный код по отсылке сообщений на отладочный с использованием individual_trace
.
Добавлю, что individual_trace
может использоваться как с отложенными, так и с периодическими сообщениями.
Ну и, кстати говоря, в реализации индивидуальной трассировки использовался вот этот механизм, который в свое время показался весьма неоднозначным, но с течением времени находит все больше и больше разнообразных применений.
Устранена проблема с лимитами сообщений и ограничением на время пребывания агента в определенном состоянии
Некоторое время назад выяснилось, что вот такой вот простой код не работает:
class demo final : public so_5::agent_t
{
state_t st_working{this, "working"};
state_t st_waiting{this, "waiting"};
public:
demo(context_t ctx) : so_5::agent_t{
ctx + limit_then_drop<some_msg>(10) // Задаем лимит для сообщения
}
{}
void so_define_agent() override {
st_working.time_limit(250ms, st_waiting); // Ограничение на время внутри состояния.
...
}
void so_evt_start() override {
...
st_working.activate(); // Бада-бум-бадам-бам-бам!
}
};
Проблема в том, что когда для агента определяются лимиты для сообщений, то агент может подписаться только на те сообщения, для которых были определены лимиты. Попытка сделать подписку на любое другое сообщение ведет к выбросу исключения.
Как раз это и происходит в ситуации, когда активируется состояние, для которого задан time_limit
. Под капотом у SObjectizer-а в этом случае идет подписка агента на специальное внутреннее сообщение, для которого лимит не задан.
Это приводит к выбросу исключения. Но исключение бросается в noexcept-контексте, что ведет к вызову std::terminate
и убийству всего приложения.
Допущен этот просчет был очень давно, но в дикой природе проявился относительно недавно. И вот при работе над версией 5.8.3 дошли руки его исправить.
Теперь если для агента описываются лимиты, то неявно добавляется еще один псевдо-лимит, как раз для внутреннего сообщения, необходимого для time_limit
. Поэтому сейчас при совместном использовании message limits и time_limit
исключение не выбрасывается.
Новое в so5extra-1.6.2
Подписка на сообщения по их базовому типу
С самого-самого начала SObjectizer был построен вокруг того, что подписка делается по конкретному типу сообщения. Т.е. если у нас есть сообщения A
и B
, то подписка на A
-- это только подписка на сообщения типа A
, а подписка на B
-- это только подписка на сообщения типа B
. Даже если B
наследуется от A
получить экземпляр B
через подписку на A
в SObjectizer нельзя. Вот нельзя и все. Точка.
Вопросы о том, а можно ли определить иерархию типов сообщений чтобы подписаться на корень этой иерархии, получая при этом по подписке любого наследника, задавались время от времени. С неутешительным ответом: вот нельзя и все. Точка.
Но раз вопросы задавались, значит это было кому-то нужно. А раз это кому-то нужно, то почему бы не попробовать эту потребность удовлетворить?
Такие попытки были, одна из них даже была описана здесь, на Хабре. И вот спустя три с половиной года предпринята еще одна, которая, вроде бы, дала ожидаемый результат (ну как тут не вспомнить афоризм "обещанного три года ждут" -- народная мудрость она ведь на многовековой опыт опирается).
Итак, в so5extra-1.6.2 добавлена работающая схема получения сообщения производного типа по подписке на базовый тип. Ниже я постараюсь описать как это выглядит в коде, подробности же можно найти в официальной Wiki проекта, ну или спросить в комментариях.
Постановка задачи
Предположим, что у нас есть задача собирать фотоснимки с нескольких камер. Камеры от разных поставщиков и могут сопровождать кадры различной дополнительной информацией. Поэтому нам было бы выгодно выстроить иерархию сообщений с изображениями:
// Базовый класс для всех фотоснимков.
struct basic_image : public so_5::message_t {...};
// Базовый класс для всех фотоснимков от камер производителя №1.
struct camera_vendor_1_image : public basic_image {...};
// Базовый класс для всех фотоснимков от камер производителя №2.
struct camera_vendor_2_image : public basic_image {...};
// Конкретные классы сообщений для камер производителя №1.
struct vendor_1_rgb_image : public camera_vendor_1_image {...};
struct vendor_1_bw_image : public camera_vendor_1_image {...};
// Конкретные классы сообщения для камер производителя №2.
struct vendor_2_infrared_image : public camera_vendor_2_image {...};
struct vendor_2_rbg_image : public camera_vendor_2_image {...};
... // И т.д., и т.п.
Нужно сделать так, чтобы подписка на basic_image
позволяла получать сообщения vendor_1_rgb_image
, vendor_1_bw_image
, vendor_2_infrared_image
, vendor_2_rbg_image
и т.д.
А подписка на camera_vendor_1_image
позволяла бы получать сообщения vendor_1_rgb_image
, vendor_1_bw_image
, но не vendor_2_infrared_image
, vendor_2_rbg_image
.
Шаг 1: описание иерархии сообщений
К сожалению, из-за отсутствия в C++17 рефлексии, придется описывать иерархию наших сообщений хитрым и несколько утомительным образом.
Первое, что нужно сделать -- это определить корень иерархии:
#include <so_5_extra/msg_hierarchy/pub.hpp>
...
struct basic_image : public so_5::extra::msg_hierarchy::root_t<basic_image> {
... // Какое-то содержимое.
};
Принципиальный момент здесь -- обязательное наследование от root_t
из so_5::extra::msg_hierarchy
.
Далее каждый класс-наследник должен быть производным сразу от двух классов: от своего непосредственного предка и от специальной примеси (mixin) node_t
из msg_hierarchy
:
struct camera_vendor_1_image
: public basic_image
, public so_5::extra::msg_hierarchy::node_t<camera_vendor_1_image, basic_image>
{...};
struct camera_vendor_2_image
: public basic_image
, public so_5::extra::msg_hierarchy::node_t<camera_vendor_2_image, basic_image>
{...};
struct vendor_1_rgb_image
: public camera_vendor_1_image
, public so_5::extra::msg_hierarchy::node_t<vendor_1_rgb_image, camera_vendor_1_image>
{...};
...
struct vendor_2_infrared_image
: public camera_vendor_2_image
, public so_5::extra::msg_hierarchy::node_t<vendor_2_infrared_image, camera_vendor_2_image>
{...};
... // И т.д., и т.п.
Но, к сожалению, в классах-нодах просто отнаследоваться от node_t
недостаточно. Нужно еще и явным образом вызывать конструктор node_t
:
struct camera_vendor_1_image
: public basic_image
, public so_5::extra::msg_hierarchy::node_t<camera_vendor_1_image, basic_image>
{
camera_vendor_1_image()
// В конструктор node_t нужно отдать ссылку на this.
: so_5::extra::msg_hierarchy::node_t<camera_vendor_1_image, basic_image>{ *this }
{}
};
...
struct vendor_1_rgb_image
: public basic_image
, public so_5::extra::msg_hierarchy::node_t<vendor_1_rgb_image, camera_vendor_1_image>
{
vendor_1_rgb_image()
// В конструктор node_t нужно отдать ссылку на this.
: so_5::extra::msg_hierarchy::node_t<vendor_1_rgb_image, camera_vendor_1_image>{ *this }
{}
};
К сожалению, это лучшее, что удалось придумать для C++17 не прибегая к помощи макросов. Возможно, когда compile-time рефлексия появится в C++ и SObjectizer переедет на стандарт, в котором эта самая рефлексия уже будет, таки получится достичь нужного эффекта с меньшим геморроем. Но это светлое будущее еще не наступило.
В общем, за неимением лучшего пока что вот так.
Шаг 2: создание демультиплексора и получение sending_mbox-а
Для того, чтобы сообщения, провязанные в иерархию наследования, должным образом доставлялись до получателей, нужно создать специальный объект-демультиплексор и получить из этого демультиплексора особый mbox для отсылаемых сообщений (этот mbox называется sending_mbox).
Делается это следующим образом:
// Сам демультиплексор.
so_5::extra::msg_hierarchy::demuxer_t<basic_image> demuxer{...}
// И его sending_mbox.
const so_5::mbox_t target_mbox = demuxer.sending_mbox();
Демультиплексор можно создать двух типов:
// Для режима multi-producer/multi-consumer.
// В этом случае у sending_mbox-а будет тип MPMC.
// Отправка мутабельных сообщений под запретом!
so_5::extra::msg_hierarchy::demuxer_t<basic_image> mpmc_demuxer{
env,
so_5::extra::msg_hierarchy::multi_consumer};
// Для режима multi-producer/single-consumer.
// В этом случае у sending_mbox-а будет тип MPSC.
// Поддерживается отправка мутабельных сообщений.
so_5::extra::msg_hierarchy::demuxer_t<basic_image> mpsc_demuxer{
env,
so_5::extra::msg_hierarchy::single_consumer};
Важно подчеркнуть, что отсылать сообщения, принадлежащие иерархии наследования, следует только в sending_mbox, полученный от демультиплексора. Тогда наследование сообщений будет учитываться при доставке сообщений подписчикам. Если же отправить, скажем, сообщение vendor_1_rgb_image
в обычный mbox, то оно будет доставлено именно как экземпляр vendor_1_rgb_image
, без учета подписок на basic_image
и camera_vendor_1_image
.
И еще один важный момент: sending_mbox поддерживает только отсылку сообщений. На него нельзя подписаться. И установить delivery filters тоже нельзя.
Шаг 3: получение у демультиплексора объекта-consmer и receiving_mbox-ов
Взятый у демультиплексора sending_mbox не допускает подписок и должен использоваться только для отправки исходящих сообщений. А вот для того, чтобы получить отосланное сообщение агенту-подписчику нужно сделать две вещи:
получить у демультиплексора специальный объект-consumer и сохранить этого consumer-а у себя;
запросить у consumer-а по одному специальному receiving_mbox-у для каждого типа получаемого сообщения.
В коде это выглядит проще, чем звучит:
// Агент, который обрабатывает сообщения от камер.
class message_processor final : public so_5::agent_t
{
// Объект-consumer, который отвечает за связь с демультиплексором.
// Лучше всего связать время жизни consumer-а с временем жизни
// агента-подписчика.
so_5::extra::msg_hierarchy::consumer_t<basic_image> m_consumer;
...
public:
message_processor(
context_t ctx,
so_5::extra::msg_hierarchy::demuxer_t<basic_image> & demuxer,
...)
: so_5::agent_t{ std::move(ctx) }
// Получаем consumer-а.
, m_consumer{ demuxer.allocate_consumer() }
, ...
{}
void so_define_agent() override
{
// Для каждого типа получаемого сообщения нужен свой receiving_mbox.
so_subscribe(m_consumer.receiving_mbox<vendor_1_rgb_image>())
.event([this](mhood_t<vendor_1_rgb_image> cmd) {...});
so_subscribe(m_consumer.receiving_mbox<camera_vendor_1_image>())
.event([this](mhood_t<camera_vendor_1_image> cmd) {...});
so_subscribe(m_consumer.receiving_mbox<basic_image>())
.event([this](mhood_t<basic_image> cmd) {...});
...
}
};
По сути, каждый объект-consumer -- это специальная связь между агентом-получателем и демультиплексором. Пока эта связь существует (т.е. пока живет consumer) демультиплексор знает, что у него есть именно этот получатель для сообщений. Когда consumer уничтожается (что обычно происходит при дерегистрации агента, владеющего consumer-ом) связь разрывается и демультиплексор перестает обслуживать этого получателя.
Еще одна важная роль consumer-а -- предотвращение доставок одной и той же копии сообщения через подписки на базовые классы.
Так, в примере выше агент message_processor
сделал три подписки на сообщения из одной иерархии. Когда в sending_mbox отправляется сообщение vendor_1_rgb_image
, то могут быть задействованы все три подписки:
подписка на
vendor_1_rgb_image
потому, что типы точно совпадают;подписка на
camera_vendor_1_image
потому, что это непосредственный базовый тип дляvendor_1_rgb_image
;подписка на
basic_image
потому, что это корень иерархии.
Но вряд ли агенту message_processor
нужно получать и обрабатывать один и тот же экземпляр трижды.
Именно благодаря тому, что message_processor
получает receiving_mbox-ы через одного и того же consumer-а, демультиплексор понимает, что все эти три mbox-а принадлежат одному подписчику и сообщение ему нужно доставлять только единожды.
А вот если бы агент создал двух consumer-ов и сделал разные подписки через них:
class strange_message_processor final : public so_5::agent_t
{
so_5::extra::msg_hierarchy::consumer_t<basic_image> m_consumer1;
so_5::extra::msg_hierarchy::consumer_t<basic_image> m_consumer2;
...
public:
strange_message_processor(
context_t ctx,
so_5::extra::msg_hierarchy::demuxer_t<basic_image> & demuxer,
...)
: so_5::agent_t{ std::move(ctx) }
// Получаем consumer-ов.
, m_consumer1{ demuxer.allocate_consumer() }
, m_consumer2{ demuxer.allocate_consumer() }
, ...
{}
void so_define_agent() override
{
so_subscribe(m_consumer1.receiving_mbox<vendor_1_rgb_image>())
.event([this](mhood_t<vendor_1_rgb_image> cmd) {...});
so_subscribe(m_consumer2.receiving_mbox<camera_vendor_1_image>())
.event([this](mhood_t<camera_vendor_1_image> cmd) {...});
...
}
};
то тогда одно и тоже сообщение vendor_1_rbg_image
ему было бы доставлено дважды, через две разные подписки. Причем заранее неизвестно в каком порядке бы эти доставки случились бы.
Сам объект демультиплексор можно долго не хранить
Если нам демультиплексор нужен только для того, чтобы получить sending_mbox и создать нескольких агентов-подписчиков, то временем жизни демультиплексора можно и не заморачиваться, просто создаем его как временный объект на стеке:
so_5::extra::msg_hierarchy::demuxer_t<basic_image> demuxer{...};
const so_5::mbox_t sending_mbox = demuxer.sending_mbox();
// Создаем кооперацию с подписчиками.
env.introduce_coop([&](so_5::coop_t & coop) {
coop.make_agent<message_processor>(demuxer, ...);
coop.make_agent<image_capturer>(demuxer, ...);
coop.make_agent<image_archiver>(demuxer, ...);
...
});
После того, как все заинтересованные стороны получили от демультиплексора все нужное (кто-то sending_mbox, кто-то consumer-ов) сам демультиплексор может быть уничтожен. Его отсутствие не повлияет на доставку сообщений подписчикам.
Но вот если нам требуется время от времени создавать новых подписчиков, то тогда время жизни демультиплексора нужно продлить. Например, можно создать его через std::make_shared
и сохранить умный указатель во всех местах, где происходит создание новых подписчиков.
Как это выглядит в коде?
Пример того, как это выглядит в коде можно увидеть на github: тыц.
Можно обратить внимание на то, что агент pinger_t
подписывается на тип abstract_pong
, а агент ponger_t
подписывается на тип abstract_ping
, но отсылаются сообщения-наследники pong
и ping
.
Почему эта функциональность была добавлена в so5extra, а не в SObjectizer?
Не смотря на то, что подобная фича на практике требовалась редко, все-таки она выглядит достаточно фундаментальной и значительно расширяющей возможности SObjectizer-а. По крайней мере на это хочется надеятся.
А раз эта фича выглядит столь фундаментальной, то почему же она не была добавлена сразу в SObjectizer?
Причина в том, что нам хочется держать SObjectizer максимально стабильным и при
этом минималистичным. Когда что-то попадает в SObjectizer, то жить оно там будет не один год. И если в ядро SObjectizer попадает что-то не сильно удачное (как это было, например, с первой версией синхронного взаимодействия агентов), то нам же самим потом придется это сопровождать из версии в версию, пока не представится возможность разрушить наслоения копролитов и отстроить заново (как это произошло в 2019-ом году с версией 5.6 после пяти лет плавного развития ветки 5.5).
Тогда как применительно к so5extra таких требований нет. Сейчас для нас so5extra -- это прежде всего полигон для проверки того, что может быть нужно пользователям SObjectizer-а, но далеко не факт, что заслуживает включения в ядро SObjectizer-а.
На данный момент непонятно что у msg_hierarchy с долгосрочными перспективами. Попробовали сделать msg_hierarchy и у нас это заработало. Теперь время посмотреть пригодится ли это кому-то в реальной жизни. Если все будет хорошо, то со временем переместим msg_hierarchy в ядро SObjectizer-а, как это произошло с unique_subscribers_mbox-ом.
Если же обнаружатся какие-то серьезные косяки, то ничего страшного. Ну появится в so5extra сперва msg_hierarchy2, а затем msg_hierarchy3, ну и хвалавсевышнему.
Так что приносим извинения всем, кто хотел бы попробовать msg_hierarchy в деле, но не горит желанием завязывать свой проект еще и на so5extra. Как бы мы как бы понимаем вашу как бы боль, но у нас есть как бы свои как бы принципы.
Заключение
Спасибо всем, кто прочитал статью полностью, очень надеюсь, что вы нашли для себя что-то интересное. Если нет, то не сочтите за труд и оставьте в комментариях свои пожелания о том, что вам интересно было бы узнать.
Но еще большее спасибо хочу сказать тем, кто находит время и возможность сообщить нам о своей боли при использовании SObjectizer-а. Именно эта информация и позволяет нам двигаться дальше. И SO-5.8.3+so5extra-1.6.2 являются лучшим доказательством того, что мы можем воплотить лишь те ваши хотелки, о которых мы узнали. Если же вы не говорите, что вам нужно, то мы и не сможем вам этого дать :(
Так что предлагаю не стесняться и говорить о том, что вы хотели бы видеть в SObjectizer.