Новое в SObjectizer-5.5.23: исполнение желаний или ящик Пандоры?


Данная статья является продолжением опубликованной месяц назад статьи-размышлении «Легко ли добавлять новые фичи в старый фреймворк? Муки выбора на примере развития SObjectizer-а«. В той статье описывалась задача, которую мы хотели решить в очередной версии SObjectizer-а, рассматривались два подхода к ее решению и перечислялись достоинства и недостатки каждого из подходов.

Прошло время, один из подходов был воплощен в жизнь и новые версии SObjectizer-а, а также сопутствующего ему проекта so_5_extra, уже, что называется «задышали полной грудью». Можно в буквальном смысле брать и пробовать.

Сегодня же мы поговорим о том, что было сделано, зачем это было сделано, к чему это привело. Если кому-то интересно следить за тем, как развивается один из немногих живых, кросс-платформенных и открытых акторных фреймворков для C++, милости прошу под кат.

С чего все началось?

Начиналось все с попытки решить проблему гарантированной отмены таймеров. Суть проблемы в том, что когда отсылается отложенное или периодическое сообщение, то программист может отменить доставку сообщения. Например:

auto timer_id = so_5::send_periodic<my_message>(my_agent, 10s, 10s, ...); ... // Что-то делаем. // Понимаем, что периодическое сообщение my_message больше нам не нужно. timer_id.release(); // Теперь таймер не будет отсылать my_message.

После вызова timer_id.release() таймер больше не будет отсылать новые экземпляры сообщения my_message. Но те экземпляры, которые уже были отосланы и попали в очереди получателей, никуда не денутся. Со временем они будут извлечены из этих самых очередей и будут переданы агентам-получателям для обработки.

Проблема эта является следствием базовых принципов работы SObjectizer-5 и не имеет простого решения из-за того, что SObjectizer не может изымать сообщения из очередей. Не может потому, что в SObjectizer очереди принадлежат диспетчерам, диспетчеры бывают разные, очереди у них также организованы по разному. В том числе бывают диспетчеры, которые не входят в состав SObjectizer-а и SObjectizer в принципе не может знать, как эти диспетчеры работают.

В общем, есть вот такая особенность у родных таймеров SObjectizer-а. Не то, чтобы она слишком уж портила жизнь разработчикам. Но некоторую дополнительную внимательность нужно проявлять. Особенно новичкам, которые только знакомятся с фреймворком.

И вот, наконец, руки дошли до того, чтобы предложить решение для этой проблемы.

Какой путь решения был выбран?

В предыдущей статье рассматривалось два возможных варианта. Первый вариант не требовал модификаций механизма доставки сообщений в SObjectizer-е, но зато требовал от программиста явным образом изменять тип отсылаемого/получаемого сообщения.

Второй вариант требовал модификации механизма доставки сообщений SObjectizer-а. Именно этот путь и был выбран, поскольку он позволял прятать от получателя сообщения тот факт, что сообщение было отослано каким-то специфическим образом.

Что изменилось в SObjectizer?

Новое понятие: конверт с сообщением внутри

Первая составляющая реализованного решения — это добавление в SObjectizer такого понятия, как конверт (envelope). Конверт — это специальное сообщение, внутри которого лежит актуальное сообщение (payload). SObjectizer доставляет конверт с сообщением до получателя почти что обычным способом. Принципиальная разница в обработке конверта обнаруживается лишь на самом последнем этапе доставки:

  • при доставке обычного сообщения у агента-получателя просто ищется обработчик для данного типа сообщения и, если такой обработчик найден, то найденный обработчик вызывается и ему отдается доставленное сообщение в качестве параметра;
  • а при доставке конверта с сообщением после того, как обработчик будет найден, сперва делается попытка достать сообщение из конверта. И только если конверт отдал хранящееся в нем сообщение, только тогда вызывается обработчик.

Здесь есть два ключевых момента, которые оказывают серьезнейшее влияние на то, для чего и как могут использоваться конверты с сообщениями.

Первый ключевой момент в том, что у конверта сообщение запрашивается только тогда, когда у получателя найден обработчик для сообщения. Т.е. только тогда, когда сообщение действительно доставлено до получателя и получатель вот прямо здесь и сейчас будет это сообщение обрабатывать.

Второй ключевой момент здесь в том, что конверт может не отдать находящееся в нем сообщение. Т.е., например, конверт может проверить текущее время и решить, что все сроки доставки были пропущены и, поэтому, сообщение перестало быть актуальным и обрабатывать его нельзя. Посему конверт не отдаст сообщение наружу. Соответственно, SObjectizer просто проигнорирует этот конверт и никаких дополнительных действий предпринимать не будет.

Что из себя представляет конверт?

Конверт — это реализация интерфейса envelope_t, который определен следующим образом:

class SO_5_TYPE envelope_t : public message_t    {    public:       ... // Конструкторы-деструкторы.        // Хук для случая, когда сообщение доставлено до получателя и       // получатель готов обработать его.       virtual void handler_found_hook(          handler_invoker_t & invoker ) noexcept = 0;        // Хук для случая, когда сообщение должно быть трансформированно       // из одно представления в другое.       virtual void transformation_hook(          handler_invoker_t & invoker ) noexcept = 0;     private :       kind_t so5_message_kind() const noexcept override          { return kind_t::enveloped_msg; }    };

Т.е. конверт — это, по сути такое же сообщение, как и все остальные. Но со специальным признаком, который и возвращается методом so5_message_kind().

Программист может разрабатывать свои конверты наследуясь от envelope_t (или, что более удобно, от so_5::extra::enveloped_msg::just_envelope_t) и переопределяя методы-хуки handler_found_hook() и transformation_hook().

Внутри методов-хуков разработчик конверта решает, хочет ли он отдать находящееся внутри конверта сообщение для обработки/трансформации или не хочет. Если хочет, то разработчик должен вызвать метод invoke() и объекта invoker. Если не хочет, то не вызывает, в этом случае конверт и его содержимое будет проигнорированно.

Как с помощью конвертов решается проблема с отменой таймеров?

Решение, которое сейчас реализовано в so_5_extra в виде пространства имен so_5::extra::revocable_timer, очень простое: при особой отсылке отложенного или периодического сообщения создается специальный конверт, внутри которого находится не только само сообщение, но и атомарный флаг revoked. Если этот флаг сброшен, то сообщение считается актуальным. Если выставлен, то сообщение считается отозванным.

Когда у конверта вызывается метод-хук, то конверт проверяет значение флага revoked. Если флаг выставлен, то конверт не отдает сообщение наружу. Тем самым, обработка сообщения не выполняется даже если таймер уже успел поместить сообщение в очередь получателя.

Расширение интерфейса abstract_message_box_t

Добавление интерфейса envelope_t — это только одна часть реализации конвертов в SObjectizer. Вторая часть — это учет факта существования конвертов в механизме доставки сообщений внутри SObjectizer-а.

Тут, к сожалению, не обошлось без внесения видимых для пользователя изменений. В частности, в класс abstract_message_box_t, который определяет интерфейс всех почтовых ящиков в SObjectizer-е, потребовалось добавить еще один виртуальный метод:

virtual void do_deliver_enveloped_msg(    const std::type_index & msg_type,    const message_ref_t & message,    unsigned int overlimit_reaction_deep );

Этот метод отвечает за доставку до получателя конверта message с сообщением типа msg_type внутри. Такая доставка может отличаться в деталях реализации в зависимости от того, что это за mbox.

При добавлении do_deliver_enveloped_msg() в abstract_message_box_t у нас был выбор: сделать его чистым виртуальным методом или же предложить какую-то реализацию по умолчанию.

Если бы мы сделали do_deliver_enveloped_msg() чистым виртуальным методом, то мы бы поломали совместимость между версиями SObjectizer в ветке 5.5. Ведь тогда тем пользователям, которые написали собственные реализации mbox-ов, пришлось бы при переходе на SObjectizer-5.5.23 модифицировать собственные mbox-ы, иначе бы не удалось пройти компиляцию с новой версией SObjectizer-а.

Нам этого не хотелось, поэтому мы не стали делать do_deliver_enveloped_msg() чистым виртуальным методом в v.5.5.23. Он имеет реализацию по умолчанию, которая просто бросает исключение. Т.о., кастомные пользовательские mbox-ы смогут нормально продолжать работу с обычными сообщениями, но будут автоматически отказываться принимать конверты. Мы сочли такое поведение более приемлемым. Тем более, что на начальном этапе вряд ли конверты с сообщениями будут применяться широко, да и маловероятно что в «дикой природе» часто встречаются кастомные реализации SObjectizer-овских mbox-ов 😉

Кроме того, существует далеко не нулевая вероятность, что в последующих мажорных версиях SObjectizer-а, где мы не будем оглядываться на совместимость с веткой 5.5, интерфейс abstract_message_box_t претерпит серьезные изменения. Но это мы уже забегаем далеко вперед…

Как отсылать конверты с сообщениями

Сам SObjectizer-5.5.23 не предоставляет простых средств отсылки конвертов. Предполагается, что под конкретную задачу разрабатывается конкретный тип конверта и соответствующие инструменты для удобной отсылки конвертов конкретного типа. Пример этого можно увидеть в so_5::extra::revocable_timer, где нужно не только отослать конверт, но и отдать пользователю специальный timer_id.

Для более простых ситуаций можно воспользоваться средствами из so_5::extra::enveloped_msg. Например, вот так выглядит отсылка сообщения с заданным ограничением на время его доставки:

 // make создает экземпляр сообщения для доставки. so_5::extra::enveloped_msg::make<my_message>(... /* Параметры для конструктора */)    // envelope помещает созданное только что сообщение в конверт нужного типа.    // Значение 5s передается в конструктор конверта вместе с экземпляром сообщения.    .envelope<so_5::extra::enveloped_msg::time_limited_delivery_t>(5s)    // А вот и отсылка конверта с сообщением адресату.    .send_to(destination);

Чтобы было совсем весело: конверты в конвертах

Конверты предназначены для переноса внутри себя каких-то сообщений. Но каких?

Любых.

И это подводит нас к интересному вопросу: а можно ли вложить конверт внутрь другого конверта?

Да, можно. Сколько угодно. Глубина вложенности ограничена только здравым смыслом разработчика и глубиной стека для рекурсивного вызова handler_found_hook/transformation_hook.

При этом SObjectizer идет навстречу разработчикам собственных конвертов: конверт не должен думать о том, что у него внутри — конкретное сообщение или другой конверт. Когда у конверта вызывают метод-хук и конверт решает, что он может отдать свое содержимое, то конверт просто вызывает invoke() у handler_invoker_t и передает в invoke() ссылку на свое содержимое. А уже invoke() внутри сам разберется, с чем он имеет дело. И если это еще один конверт, то invoke() сам вызовет у этого конверта нужный метод-хук.

С помощью уже показанного выше инструментария из so_5::extra::enveloped_msg пользователь может сделать несколько вложенных конвертов вот таким образом:

so_5::extra::enveloped_msg::make<my_message>(...)    // Конверт, который будет внутри и который содержит сообщение my_message.    .envelope<inner_envelope_type>(...)    // Конверт, который будет содержать конверт типа inner_envelope_type.    .envelope<outer_envelope_type>(...)    .send_to(destination);

Несколько примеров использования конвертов

Теперь, после того, как мы прошлись по внутренностям SObjectizer-5.5.23 пора бы уже перейти к более полезной для пользователей, прикладной части. Ниже рассматривается несколько примеров, которые либо базируются на том, что уже реализовано в so_5_extra, либо используют инструменты из so_5_extra.

Отзывные таймеры

Поскольку вся эта кухня с конвертами затевалась ради решения проблемы гарантированного отзыва таймерых сообщений, то давайте посмотрим, что в итоге получилось. Будем использовать пример из so_5_extra-1.2.0, который задействует инструменты из нового пространства имен so_5::extra::revocable_timer:

Код примера с отзывными таймерами

#include <so_5_extra/revocable_timer/pub.hpp>  #include <so_5/all.hpp>  namespace timer_ns = so_5::extra::revocable_timer;  class example_t final : public so_5::agent_t {    // Набор сигналов, которые мы будем использовать для отсылки    // отложенных и периодического сообщения.    struct first_delayed final : public so_5::signal_t {};    struct second_delayed final : public so_5::signal_t {};    struct last_delayed final : public so_5::signal_t {};     struct periodic final : public so_5::signal_t {};     // Идентификаторы для таймерных сообщений.    timer_ns::timer_id_t m_first;    timer_ns::timer_id_t m_second;    timer_ns::timer_id_t m_last;    timer_ns::timer_id_t m_periodic;  public :    example_t( context_t ctx ) : so_5::agent_t{ std::move(ctx) }    {       so_subscribe_self()          .event( &example_t::on_first_delayed )          .event( &example_t::on_second_delayed )          .event( &example_t::on_last_delayed )          .event( &example_t::on_periodic );    }     void so_evt_start() override    {       using namespace std::chrono_literals;        // Отсылаем три сигнала как отложенные сообщения...       m_first = timer_ns::send_delayed< first_delayed >( *this, 100ms );       m_second = timer_ns::send_delayed< second_delayed >( *this, 200ms );       m_last = timer_ns::send_delayed< last_delayed >( *this, 300ms );       // ...и один как периодическое сообщение.       m_periodic = timer_ns::send_periodic< periodic >( *this, 75ms, 75ms );        // Блокируем агента на 220ms. За это время в очередь агента       // должны попасть сигналы first_delaye, second_delayed и       // несколько экземпляров сигнала periodic.       std::cout << "hang the agent..." << std::flush;       std::this_thread::sleep_for( 220ms );       std::cout << "done" << std::endl;    }  private :    void on_first_delayed( mhood_t<first_delayed> )    {       std::cout << "first_delayed received" << std::endl;        // Отменяем доставку second_delayed и periodic.       // Агент не должен получить эти сигналы не смотря на то, что       // они уже стоят в очереди сообщений агента.       m_second.revoke();       m_periodic.revoke();    }     void on_second_delayed( mhood_t<second_delayed> )    {       std::cout << "second_delayed received" << std::endl;    }     void on_last_delayed( mhood_t<last_delayed> )    {       std::cout << "last_delayed received" << std::endl;       so_deregister_agent_coop_normally();    }     void on_periodic( mhood_t<periodic> )    {       std::cout << "periodic received" << std::endl;    } };  int main() {    so_5::launch( [](so_5::environment_t & env) {       env.register_agent_as_coop( "example", env.make_agent<example_t>() );    } );     return 0; }

Что мы здесь имеем?

У нас есть агент, который сперва инициирует несколько таймерных сообщений, а потом блокирует свою рабочую нить на некоторое время. За это время таймер успевает поставить в очередь агента несколько заявок в результате сработавших таймеров: несколько экземпляров periodic, по одному экземпляру first_delayed и second_delayed.

Соответственно, когда агент разблокирует свою нить, он должен получить первый periodic и first_delayed. При обработке first_delayed агент отменяет доставку periodic-а и second_delayed. Поэтому эти сигналы до агента доходить не должны вне зависимости от того, есть ли они уже в очереди агента или нет (а они есть).

Смотрим на результат работы примера:

hang the agent...done periodic received first_delayed received last_delayed received

Да, так и есть. Получили первый periodic и first_delayed. Затем нет ни periodic-а, ни second_delayed.

А вот если в примере заменить «таймеры» из so_5::extra::revocable_timer на штатные таймеры из SObjectizer, то результат будет другой: до агента все-таки дойдут те экземпляры сигналов periodic и second_delayed, которые уже попали к агенту в очередь.

Сообщения с ограничениями на время доставки

Еще одна полезная, временами, штука, которая станет доступной в so_5_extra-1.2.0 — это доставка сообщения с ограничением по времени. Например, агент request_handler отсылает сообщение verify_signature агенту crypto_master. При этом request_handler хочет, чтобы verify_signature был доставлен в течении 5 секунд. Если это не произошло, то смысла в обработке verity_signature уже не будет, агент request_handler уже прекратит свою работу.

А агент crypto_master — это такой товарищ, который любит оказываться «бутылочным горлышком»: временами начинает притормаживать. В такие момент у него в очереди скапливаются сообщения, вроде вышеуказанного verify_signature, которые могут ждать до тех пор, пока crypto_master-у не полегчает.

Предположим, что request_handler отослал сообщение verify_signature агенту crypto_master, но тут crypto_master-у поплохело о он «залип» на 10 секунд. Агент request_handler уже «отвалился», т.е. уже отослал всем отказ в обслуживании и завершил свою работу. Но ведь сообщение verify_signature в очереди crypto_master-а осталось! Значит, когда crypto_master «отлипнет», то он возьмет данное сообщение и будет это сообщение обрабатывать. Хотя это уже не нужно.

С помощью нового конверта so_5::extra::enveloped_msg::time_limited_delivery_t мы можем решить данную проблему: агент request_handler отошлет verify_signature вложенное в конверт time_limited_delivery_t с ограничением на время доставки:

so_5::extra::enveloped_msg::make<verify_signature>(...)    .envelope<so_5::extra::enveloped_msg::time_limited_delivery_t>(5s)    .send_to(crypto_master_mbox);

Теперь если crypto_master «залипнет» и не успеет добраться до verify_signature за 5 секунд, то конверт просто не отдаст это сообщение на обработку. И crypto_master не будет делать работу, которая уже никому не нужна.

Отчеты о доставке сообщений до получателя

Ну и напоследок пример любопытной штуки, которая не реализована штатно ни в SObjectizer, ни в so_5_extra, но которую можно сделать самостоятельно.

Иногда хочется получать от SObjectizer-а что-то вроде «отчета о доставке» сообщения до получателя. Ведь одно дело, когда сообщение до получателя дошло, но получатель по каким-то своим причинам на него не среагировал. Другое дело, когда сообщение вообще до получателя не дошло. Например, было заблокировано механизмом защиты агентов от перегрузки. В первом случае сообщение, на которое мы не дождались ответа, можно не перепосылать. А вот во втором случае может иметь смысл перепослать сообщение спустя некоторое время.

Сейчас мы рассмотрим, как посредством конвертов можно реализовать простейший механизм «отчетов о доставке».

Итак, сначала сделаем необходимые подготовительные действия:

#include <so_5_extra/enveloped_msg/just_envelope.hpp> #include <so_5_extra/enveloped_msg/send_functions.hpp>  #include <so_5/all.hpp>  using namespace std::chrono_literals;  namespace envelope_ns = so_5::extra::enveloped_msg;  using request_id_t = int; 

Теперь мы можем определить сообщения, которые будут использоваться в примере. Первое сообщение — это запрос для выполнения каких-то нужных нам действий. А второе сообщение — это подтверждение того, что первое сообщение дошло до получателя:

struct request_t final {    request_id_t m_id;    std::string m_data; };  struct delivery_receipt_t final {    // Это значение request_t::m_id из соответствующего request_t.    request_id_t m_id; };

Далее мы можем определить агента processor_t, который будет обрабатывать сообщения типа request_t. Но обрабатывать будет с имитацией «залипания». Т.е. он обрабатывает request_t, после чего меняет свое состояние с st_normal на st_busy. В состоянии st_busy он ничего не делает и игнорирует все сообщения, которые к нему прилетают.

Это означает, что если агенту processor_t отослать подряд три сообщения request_t, то первое он обработает, а два других будут выброшены, т.к. при обработке первого сообщения агент уйдет в st_busy и проигнорирует то, что к нему будет приходить пока он находится в st_busy.

В st_busy агент processor_t проведет 2 секунды, после чего вновь вернется в st_normal и будет готов обрабатывать новые сообщения.

Вот как агент processor_t выглядит:

class processor_t final : public so_5::agent_t {    // Нормальное состояние агента. В этом состоянии выполняется    // обработка входящих сообщений.    state_t st_normal{this, "normal"};    // Состояние "я занят". Новые сообщения игнорируются.    state_t st_busy{this, "busy"};  public:    processor_t(context_t ctx) : so_5::agent_t{std::move(ctx)}    {       this >>= st_normal;        st_normal.event(&processor_t::on_request);        // Для этого состояния нет подписок, но есть лимит времени.       // Через 2 секунды после входа, автоматический возврат в st_normal.       st_busy.time_limit(2s, st_normal);    }  private:    void on_request(mhood_t<request_t> cmd)    {       std::cout << "processor: on_request(" << cmd->m_id << ", "             << cmd->m_data << ")" << std::endl;        this >>= st_busy;    } };

Теперь мы можем определить агента requests_generator_t, у которого есть пачка запросов, которые нужно доставить до processor_t. Агент request_generator_t раз в 3 секунды отправляет всю пачку, а затем ждет подтверждения о доставке в виде delivery_receipt_t.

Когда delivery_recept_t приходит, агент requests_generator_t выбрасывает доставленный запрос из пачки. Если пачка совсем опустела, то работа примера завершается. Если же еще что-то осталось, то оставшаяся пачка будет отослана заново когда наступит следующее время перепосылки.

Итак, вот код агента request_generator_t. Он довольно объемный, но примитивный. Обратить внимание можно разве что на внутренности метода send_requests(), в котором отсылаются сообщения request_t, вложенные в специальный конверт.

Код агента requests_generator_t

class requests_generator_t final : public so_5::agent_t {    // Почтовый ящик обработчика запросов.    const so_5::mbox_t m_processor;     // Пачка запросов, для которых еще нет подтверждения о доставке.    std::map<request_id_t, std::string> m_requests;     struct resend_requests final : public so_5::signal_t {};  public:    requests_generator_t(context_t ctx, so_5::mbox_t processor)       :  so_5::agent_t{std::move(ctx)}       ,  m_processor{std::move(processor)}    {       so_subscribe_self()          .event(&requests_generator_t::on_delivery_receipt)          .event(&requests_generator_t::on_resend);    }     void so_evt_start() override    {       // Формируем первоначальную пачку запросов.       m_requests.emplace(0, "First");       m_requests.emplace(1, "Second");       m_requests.emplace(2, "Third");       m_requests.emplace(3, "Four");        // Начинаем рассылку.       send_requests();    }  private:    void on_delivery_receipt(mhood_t<delivery_receipt_t> cmd)    {       std::cout << "request delivered: " << cmd->m_id << std::endl;       m_requests.erase(cmd->m_id);        if(m_requests.empty())          // Запросов больше не досталось. Работу прекращаем.          so_deregister_agent_coop_normally();    }     void on_resend(mhood_t<resend_requests>)    {       std::cout << "time to resend requests, pending requests: "             << m_requests.size() << std::endl;        send_requests();    }     void send_requests()    {       for(const auto & item : m_requests)       {          std::cout << "sending request: (" << item.first << ", "                << item.second << ")" << std::endl;           envelope_ns::make<request_t>(item.first, item.second)                .envelope<custom_envelope_t>(so_direct_mbox(), item.first)                .send_to(m_processor);       }        // Отложенное сообщение чтобы повторить отсылку через 3 секунды.       so_5::send_delayed<resend_requests>(*this, 3s);    } };

Вот теперь у нас есть сообщения и есть агенты, которые с помощью этих сообщений должны общаться. Осталась самая малость — как-то заставить прилетать сообщения delivery_receipt_t при доставке request_t до processor_t.

Делается это с помощью вот такого конверта:

class custom_envelope_t final : public envelope_ns::just_envelope_t {    // Куда присылать отчет о доставке.    const so_5::mbox_t m_to;     // ID доставленного запроса.    const request_id_t m_id;  public:    custom_envelope_t(so_5::message_ref_t payload, so_5::mbox_t to, request_id_t id)       :  envelope_ns::just_envelope_t{std::move(payload)}       ,  m_to{std::move(to)}       ,  m_id{id}    {}     void handler_found_hook(handler_invoker_t & invoker) noexcept override    {       // Раз этот хук вызван, значит сообщение до получателя дошло.       // Можно отсылать отчет о доставке.       so_5::send<delivery_receipt_t>(m_to, m_id);       // Всю остальную работу делает базовый класс.       envelope_ns::just_envelope_t::handler_found_hook(invoker);    } };

В общем-то, здесь нет ничего сложного. Мы наследуемся от so_5::extra::enveloped_msg::just_envelope_t. Это вспомогательный тип конверта, который хранит вложенное в него сообщение и предоставляет базовую реализацию хуков
handler_found_hook() и transformation_hook(). Поэтому нам остается только сохранить внутри custom_envelope_t нужные нам атрибуты и отослать delivery_receipt_t внутри хука handler_found_hook().

Вот, собственно, и все. Если запустить данный пример, то получим следующее:

sending request: (0, First) sending request: (1, Second) sending request: (2, Third) sending request: (3, Four) processor: on_request(0, First) request delivered: 0 time to resend requests, pending requests: 3 sending request: (1, Second) sending request: (2, Third) sending request: (3, Four) processor: on_request(1, Second) request delivered: 1 time to resend requests, pending requests: 2 sending request: (2, Third) sending request: (3, Four) processor: on_request(2, Third) request delivered: 2 time to resend requests, pending requests: 1 sending request: (3, Four) processor: on_request(3, Four) request delivered: 3

В качестве дополнения нужно сказать, что на практике такой простой custom_envelope_t для формирования отчетов о доставке вряд ли подойдет. Но если кому-то интересна эта тема, то ее можно обсудить в комментариях, а не увеличивать объем статьи.

Что еще можно было бы делать с помощью конвертов?

Отличный вопрос! На который у нас самих пока нет исчерпывающего ответа. Вероятно, возможности ограничиваются разве что фантазией пользователей. Ну а если для воплощения фантазий в SObjectizer-е чего-то не хватает, то об этом можно сказать нам. Мы всегда прислушиваемся. И, что немаловажно, временами даже делаем 🙂

Интеграция агентов с mchain-ами

Если же говорить чуть более серьезно, то есть еще одна фича, которую хотелось бы временами иметь и которая даже планировалась для so_5_extra-1.2.0. Но которая, скорее всего, в релиз 1.2.0 уже не попадет.

Речь идет о том, чтобы упростить интеграцию mchain-ов и агентов.

Дело в том, что первоначально mchain-ы были добавлены в SObjectizer для того, чтобы упростить общение агентов с другими частями приложения, которые написаны без агентов. Например, есть главный поток приложения, на котором с помощью GUI идет взаимодействие с пользователем. И есть несколько агентов-worker-ов, которые выполняют фоновую «тяжелую» работу. Отослать сообщение агенту из главного потока не проблема: достаточно вызвать обычный send. А вот как передать информацию назад?

Для этого и были добавлены mchain-ы.

Но со временем выяснилось, что mchain-ы могут играть гораздо большую роль. Можно, в принципе, делать многопоточные приложения на SObjectizer-е вообще без агентов, только на mchain-ах (подробнее здесь). А еще можно использовать mchain-ы как средство балансировки нагрузки на агентов. Как механизм решения проблемы producer-consumer.

Проблема producer-consumer заключается в том, что если producer генерирует сообщения быстрее, чем их может обрабатывать consumer, то нас ждут неприятности. Очереди сообщений будут расти, со временем может деградировать производительность или вообще произойдет вылет приложения из-за исчерпания памяти.

Обычное решение, которое мы предлагали использовать в этом случае — это использовать пару агентов collector-performer. Так же можно использовать и message limits (либо как основной механизм защиты, либо как дополнение к collector-performer). Но написание collector-performer требует дополнительной работы от программиста.

А вот mchain-ы могли бы использоваться для этих целей с минимальными усилиями со стороны разработчика. Так, producer бы помещал очередное сообщение в mchain, а consumer бы забирал сообщения из этого mchain.

Но проблема в том, что когда consumer — это агент, то агенту не очень удобно работать с mchain-ом посредством имеющихся функций receive() и select(). И вот это неудобство можно было бы попробовать устранить с помощью какого-то инструмента для интеграции агентов и mchain-ов.

При разработке такого инструмента нужно будет решить несколько задачек. Например, когда сообщение приходит в mchain, то в какой момент оно должно быть из mchain-а извлечено? Если consumer свободен и ничего не обрабатывает, то можно забрать сообщение из mchain-а сразу и отдать его агенту-consumer-у. Если consumer-у уже было отослано сообщение из mchain-а, он это сообщение еще не успел обработать, но в mchain уже приходит новое сообщение… Как быть в этом случае?

Есть предположение, что конверты могут помочь в этом случае. Так, когда мы берем первое сообщение из mchain-а и отсылаем его consumer-у, то мы оборачиваем это сообщение в специальный конверт. Когда конверт видит, что сообщение доставлено и обработано, он запрашивает следующее сообщение из mchain-а (если таковое есть).

Конечно, здесь все не так просто. Но пока что выглядит вполне решаемо. И, надеюсь, подобный механизм появится в одной из следующих версий so_5_extra.

Уж не ящик ли Пандоры мы собираемся открыть?

Нужно отметить, что у нас самих добавленные возможности вызывают двойственные чувства.

С одной стороны, конверты уже позволили/позволяют сделать вещи, о которых ранее говорилось (а о чем-то просто мечталось). Например, это гарантированная отмена таймеров и ограничение на время доставки, отчеты о доставки, возможность отзыва ранее отосланного сообщения.

С другой стороны, непонятно, к чему это приведет впоследствии. Ведь из любой возможности можно сделать проблему, если начать эту возможность эксплуатировать где нужно и где не нужно. Так может мы приоткрываем ящик Пандоры и сами еще не представляем, что нас ждет?

Остается только набраться терпения и посмотреть, куда это все нас приведет.

О ближайших планах развития SObjectizer-а вместо заключения

Вместо заключения хочется рассказать о том, каким мы видим самое ближайшее (и не только) будущее SObjectizer-а. Если кого-то что-то в наших планах не устраивает, то можно высказаться и повлиять на то, как SObjectizer-5 будет развиваться.

Первые бета-версии SObjectizer-5.5.23 и so_5_extra-1.2.0 уже зафиксированы и доступны для загрузки и экспериментов. К релизу нужно будет проделать еще много работы в области документации и примеров использования. Поэтому официальный релиз планируется в первой декаде ноября. Если получится раньше, сделаем раньше.

Релиз SObjectizer-5.5.23, судя по всему, будет означать, что эволюция ветки 5.5 подходит к своему финалу. Самый первый релиз в этой ветке состоялся четыре года назад, в октябре 2014-го. С тех пор SObjectizer-5 эволюционировал в рамках ветки 5.5 без каких-либо серьезных ломающих изменений между версиями. Это было непросто. Особенно с учетом того, что все это время нам приходилось оглядываться на компиляторы, в которых была далеко не идеальная поддержка C++11.

Сейчас мы уже не видим смысла оглядываться на совместимость внутри ветки 5.5 и, особенно, на старые C++ компиляторы. То, что можно было оправдать в 2014-ом, когда C++14 еще только готовились официально принять, а C++17 еще не было на горизонте, сейчас уже выглядит совсем по-другому.

Плюс к тому, в самом SObjectizer-5.5 уже накопилось изрядное количество граблей и подпорок, которые появились из-за этой самой совместимости и которые затрудняют дальнейшее развитие SObjectizer-а.

Поэтому мы в ближайшие месяцы собираемся действовать по следующему сценарию:

1. Разработка следующей версии so_5_extra, в которую хочется добавить инструментарий для упрощения написания тестов для агентов. Будет ли это so_5_extra-1.3.0 (т.е. с ломающими изменениями относительно 1.2.0) или это будет so_5_extra-1.2.1 (т.е. без ломающих изменений) пока не понятно. Посмотрим, как пойдет. Понятно только, что следующая версия so_5_extra будет базироваться на SObjectizer-5.5.

1a. Если для следующей версии so_5_extra потребуется сделать что-то дополнительное в SObjectizer-5.5, то будет выпущена следующая версия 5.5.24. Если же для so_5_extra не нужно будет вносить доработки в ядро SObjectizer-а, то версия 5.5.23 окажется последней значимой версией в рамках ветки 5.5. Мелкие bug-fix релизы будут выходить. Но само развитие ветки 5.5 прекращается на версии 5.5.23 или 5.5.24.

2. Затем будет выпущена версия SObjectizer-5.6.0, которая откроет новую ветку. В ветке 5.6 мы вычистим код SObjectizer-а от всех накопившихся костылей и подпорок, а также от старого хлама, который давным давно помечен, как deprecated. Вероятно, какие-то вещи подвергнуться рефакторингу (например, может быть изменен abstract_message_box_t), но вряд ли кардинальному. Основные же принципы работы и характерные черты SObjectizer-5.5 в SObjectizer-5.6 останутся в том же виде.

SObjectizer-5.6 будет требовать уже C++14 (хотя бы на уровне GCC-5.5). Компиляторы Visual C++ ниже VC++ 15 (который из Visual Studio 2017) поддерживаться не будут.

Ветка 5.6 рассматривается нами как стабильная ветка SObjectizer-а, которая будет актуальна до тех пор, пока не появится первая версия SObjectizer-5.7.

Релиз версии 5.6.0 хотелось бы сделать в начале 2019-го года, ориентировочно в феврале.

3. После стабилизации ветки 5.6 мы бы хотели начать работать над веткой 5.7, в которой можно было бы пересмотреть какие-то базовые принципы работы SObjectizer-а. Например, совсем отказаться от публичных диспетчеров, оставив только приватные. Переделать механизм коопераций и их взаимоотношений родитель-потомок, тем самым избавившись от узкого места при регистрации/дерегистрации коопераций. Убрать деление на message/signal. Оставить для отсылки сообщений только send/send_delayed/send_periodic, а методы deliver_message и schedule_timer упрятать «под капот». Модифицировать механизм диспетчеризации сообщений так, чтобы либо совсем убрать dynamic_cast-ы из этого процесса, либо свести их к самому минимуму.

В общем, тут есть где развернуться. При этом SObjectizer-5.7 уже будет требовать C++17, без оглядки на C++14.

Если смотреть на вещи без розовых очков, то хорошо, если релиз 5.7.0 состоится в конце осени 2019. Т.е. основной рабочей версией SObjectizer-а на 2019-й будет ветка 5.6.

4. Параллельно всему этому будет развиваться so_5_extra. Вероятно, вместе с SObjectizer-5.6 будет выпущена версия so_5_extra-2, которая на протяжении 2019-го года будет вбирать в себя новый функционал, но на базе SObjectizer-5.6.

Таким образом мы сами видим для SObjectizer-5 поступательную эволюцию с постепенным пересмотром каких-то из базовых принципов работы SObjectizer-5. При этом мы постараемся делать это как можно более плавно, чтобы можно было с минимальными болевыми ощущениями переходить с одной версии на другую.

Однако, если кто-то хотел бы от SObjectizer-а более кардинальных и значимых изменений, то у нас есть соображения на этот счет. Если совсем коротко: можно переделать SObjectizer как угодно, вплоть до того, чтобы реализовать SObjectizer-6 для другого языка программирования. Но делать это полностью за свой счет, как это происходит с эволюцией SObjectizer-5, мы не будем.

На этом, пожалуй, все. В комментариях к предыдущей статье получилось хорошее и конструктивное обсуждение. Нам было бы полезно, если бы подобное обсуждение случилось и в этот раз. Как всегда мы готовы ответить на любые вопросы, а на толковые, так и с удовольствием.

А самым терпеливым читателям, добравшимся до этих строк большое спасибо за потраченное на прочтение статьи время.


ссылка на оригинал статьи https://habr.com/post/426983/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *