Pull to refresh

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

Reading time 21 min
Views 882


Данная статья является продолжением опубликованной месяц назад статьи-размышлении "Легко ли добавлять новые фичи в старый фреймворк? Муки выбора на примере развития 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, мы не будем.

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

А самым терпеливым читателям, добравшимся до этих строк большое спасибо за потраченное на прочтение статьи время.
Tags:
Hubs:
+3
Comments 2
Comments Comments 2

Articles