Нове в 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, то нас чекають неприємності. Черги повідомлень будуть рости, з часом може деградувати продуктивність або взагалі відбудеться виліт додатка з-за вичерпання пам’яті.

Читайте також  Ще раз про рівні Tier

Звичайне рішення, яке ми пропонували використовувати в цьому разі — це використовувати пару агентів 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, ми не будемо.

На цьому, мабуть, все. У коментарях до попередньої статті вийшло гарне і конструктивне обговорення. Нам було б корисно, якби подібне обговорення сталося і цього разу. Як завжди ми готові відповісти на будь-які питання, а на тлумачні, так і з задоволенням.

А самим терплячим читачам, що дістався до цих рядків велике спасибі за витрачений на прочитання статті час.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

Вам також сподобається...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *