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