четверг, 8 февраля 2018 г.

[prog.c++] SObjectizer v.5.5.21 и so_5_extra v.1.0.4

Мы обновили свои инструменты для упрощения разработки сложных многопоточных и/или событийно-ориентированных приложений на C++. SObjectizer обновился до версии 5.5.21, а дополнительный набор инструментов над ним -- до версии 1.0.4.

Самое главное в этом релизе -- это появление в so_5_extra такой штуки, как асинхронные операции или просто async_op. Что стало логическим завершением темы, начатой некоторое время назад (пост №1, пост №2, пост №3). Асинхронные операции значительно упрощают реализацию эпизодических однократных взаимодействий между агентами. Происходит это из-за того, что асинхронная операция берет на себя задачи по подписке на нужные сообщения при начале асинхронной операции и по удалению подписок после того, как результат операции будет получен. А так же async_op берет на себя задачи работы с отложенными сообщениями, если для операции существует лимит на время выполнения.

Ну, например, пусть мы делаем агента распределяющего обработку запросов по нескольким обработчикам запросов. Этот агент получает сообщение request_data, определяет mbox, в который нужно переслать сообщение, а затем дождаться получение из этого mbox-а либо сообщения request_successed, либо сообщения request_failed. Так же должен учитываться тайм-аут на обработку запроса. Посредством асинхронных операций это может быть реализовано вот так:

class request_manager : public so_5::agent_t {
   struct request_timed_out {
      user_id user_;
      request_id id_;
   };
   ...
   void on_successful_result(mhood_t<request_successed> cmd) {...}
   void on_failed_result(mhood_t<request_failed> cmd) {...}
   void on_timeout(mhood_t<request_timed_out> cmd) {...}
   ...
   void on_new_request(mhood_t<request_data> cmd) {
      // Определяем, кто будет обрабатывать запрос.
      const auto processor_mbox = detect_processor_for_req(*cmd);

      // Подготавливаем асинхронную обработку запроса.
      so_5::extra::async_op::time_limited::make<request_timed_out>(*this)
         .completed_on(processor_mbox, so_default_state(),
            &request_manager::on_successful_result)
         .completed_on(processor_mbox, so_default_state(),
            &request_manager::on_failed_result)
         .timeout_handler(so_default_state(),
            &request_manager::on_timeout)
         .activate(5s, cmd->user_, cmd->id_);

      // Передаем запрос конкретному исполнителю.
      so_5::send(processor_mbox, cmd);
   }
   ...
};

Достаточно читаемо, не правда ли?

А вот так это же можно было бы записать лишь с использованием штатных средств самого SObjectizer-а. В самом примитивном виде, даже без обеспечения какой-либо exception safety.

class request_manager : public so_5::agent_t {
   struct request_timed_out {
      user_id user_;
      request_id id_;
   };
   ...
   void on_successful_result(mhood_t<request_successed> cmd) {...}
   void on_failed_result(mhood_t<request_failed> cmd) {...}
   void on_timeout(mhood_t<request_timed_out> cmd) {...}
   ...
   void on_new_request(mhood_t<request_data> cmd) {
      // Определяем, кто будет обрабатывать запрос.
      const auto processor_mbox = detect_processor_for_req(*cmd);

      // Подготавливаем асинхронную обработку запроса.
      // Этот mbox будет использоваться для тайм-аута.
      const auto timeout_mbox__ = so_environment().create_mbox();
      // Это нужно дабы не дублировать код.
      auto subscription_dropper__ = [this, processor_mbox, timeout_mbox__] {
            so_drop_subscription<request_successed>(processor_mbox);
            so_drop_subscription<request_failed>(processor_mbox);
            so_drop_subscription<request_timed_out>(timeout_mbox__);
         };

      // Теперь подписка на нужные события...
      so_subscribe(processor_mbox)
         .event([this, subscription_dropper__](mhood_t<request_successed> cmd) {
               subscription_dropper__(); // Обязательно удаляем подписки.
               on_successful_result(cmd);
            })
         .event([this, subscription_dropper__](mhood_t<request_failed> cmd) {
               subscription_dropper__(); // Обязательно удаляем подписки.
               on_failed_result(cmd);
            });
      so_subscribe(timeout_mbox__)
         .event([this, subscription_dropper_](mhood_t<request_timed_out> cmd) {
               subscription_dropper__(); // Обязательно удаляем подписки.
               on_timeout(cmd);
            });

      // Ограничиваем время выполнения операции.
      so_5::send_delayed<request_timed_out>(
         so_environment(), timeout_mbox, 5s, cmd->user_, cmd->id_);

      // Передаем запрос конкретному исполнителю.
      so_5::send(processor_mbox, cmd);
   }
   ...
};

Для того, чтобы такие асинхронные операции появились в so_5_extra, пришлось добавить важную новую фичу в сам SObjectizer. Это т.н. deadletter handlers.

Когда агент получает сообщение, то происходит поиск обработчика сообщения по списку подписок. Если подписка найдена, то вызывается соответствующий обработчик. А вот что, если подписка для текущего состояния агента не найдена?

Ранее сообщение, для которого не был найден обработчик, просто выбрасывалось. А вот сейчас есть возможность повесить на сообщение типа message_type из почтового ящика mbox специальный обработчик, который называется deadletter handler. Обращаем внимание, что в отличии о обычного разработчика, который вешается на триплет (state, message_type, mbox), deadletter handler цепляется только к паре (message_type, mbox). Если обычного обработчика для триплета (current_state, message_type, mbox) не найдено, то для пары (message_type, mbox) ищется deadletter handler. Если deadletter handler найден, то он вызывается. Вызывается вне зависимости от текущего состояния.

Эта особенность позволяет использовать deadletter handler-ы в качестве обработчиков событий "по умолчанию". Т.е. если пользователь не задал специфического обработчика для какого-то сообщения, но при поступлении сообщения будет вызван deadletter handler.

В принципе, обработчики "по умолчанию" следует создавать используя возможности конечных автоматов. Например, можно поступать так:

class demo : public so_5::agent_t {
   state_t st_parent{this};
   state_t st_first{ initial_substate_of{st_parent} },
      st_second{ substate_of{st_parent} },
      st_third{ substate_of{st_parent} };

   void some_message_default_handler(mhood_t<some_msg> cmd) {...}

   virtual void so_define_agent() override {
      // Вот это и будет обработчик "по-умолчанию".
      st_parent.event(&demo::some_message_default_handler);
      // В других состояниях на some_msg можно повесить другие обработчики.
      ...
   }
};

Но это не всегда возможно. Например, если автор не предусмотрел в агенте st_parent, то как быть тогда? Вот в этом случае deadletter handler-ы и приходят на помощь. Например:

class demo : public so_5::agent_t {
   // Нет состояния st_parent.
   state_t st_first{this}, st_second{this}, st_third{this};

   void some_message_default_handler(mhood_t<some_msg> cmd) {...}

   virtual void so_define_agent() override {
      // Вот это и будет обработчик "по-умолчанию".
      so_subscribe_deadletter_handler(so_direct_mbox(), &demo::some_message_default_handler);
      // В других состояниях на some_msg можно повесить другие обработчики.
      ...
   }
};

Кроме того, можно отметить еще несколько изменений в SObjectizer-е:

  • значения параметров pause и period при отсылке отложенных/периодических сообщений теперь явным образом проверяются. Если у них отрицательные значения, то порождается исключение;
  • для state_t добавлен operator!=().

Взять дистрибутив новой версии SObjectizer-а можно из раздела Files на SourceForge. Документация по изменениям в версии 5.5.21 доступна здесь. Так же можно воспользоваться зеркалом на github. Пользователи vcpkg могут установить последнюю версию SObjectizer через vcpkg install sobjectizer.

Дистрибутив новой версии so_5_extra можно взять из раздела Files на SourceForge. Документация по изменениям в версии 1.0.4 доступна здесь.


Теперь у меня несколько просьб к тем, кто пользуется или интересуется SObjectizer-ом.

Во-первых, если вы обнаруживаете, что к какой-то функции/методу/классу в Doxygen-овской документации описание очень куцее, из которого ничего не понятно, то не сочтите за труд, покажите пальцем. Я сам не очень доволен многими Doxygen-овскими комментариями и мы пытаемся это дело улучшать. Но будет лучше, если нам будут указывать на наиболее проблемные места. Сделать это можно, например, через Feature Request на SourceForge. Или через Issues на github-е. (Построить Doxygen документацию можно самому: заходим в каталог doxygen, который находится на одном уровне с dev, запускаем doxygen и получаем документацию в dev/doc/html)

Во-вторых, мы уже на следующей неделе начнем работать над SO-5.5.22. Основная фича, которая запланирована в следующей версии, -- это поддержка параллельных состояний. Есть такая штука в иерархических конечных автоматах. Когда КА как-бы сразу находится в двух (трех-, четырех- и т.д.) независимых друг от друга состояниях. Так вот просьба такая: если вам приходилось сталкиваться с задачами, где требовались параллельные состояния, то поделитесь опытом, плиз. Чем более разнообразные примеры будут перед глазами, тем более удобное решение получится сделать.

Ну и в-третьих, если не сложно, порадуйте нас звездочкой на github-е или review на SF. Нам, конечно же, будет приятно, но не это главное. Главное то, что у многих возникает больше доверия к проектам, у которых больше разных звездочек/шильдиков/прочей-мишуры. Пусть нам больше доверяют :)


Напоследок скажу, что этот релиз оказался одним из самых сложных. Не такой сложный, как версия 5.5.19, но непростой. Слишком много было безуспешных заходов, слишком много тупиков при поиске нужного решения, слишком много просчетов, в том числе и довольно грубых. Хорошо, что все это уже позади. Теперь посмотрим, как это будет работать.

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

Комментариев нет: