Легко додавати нові фічі в старий фреймворк? Муки вибору на прикладі розвитку SObjectizer-а

Розробка безкоштовного фреймворку для потреб розробників — це специфічна тема. Якщо при цьому фреймворк живе і розвивається досить довго, то специфіки додається. Сьогодні я спробую показати це на прикладі спроби розширити функціональність «акторного» фреймворку для C++ під назвою SObjectizer.

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

Прямо зараз нам потрібно вирішити як саме додати нову фічу в чергову версію. В процесі пошуку відповідного рішення вималювалася два варіанти. Обидва виглядають цілком собі реалізованими. Але дуже вже сильно відрізняються один від одного. Як по складності і трудомісткості реалізації, так і за своїм «зовнішнім виглядом». Тобто те, з чим буде мати справу розробник, в кожному з варіантів буде виглядати по-різному. Напевно, навіть принципово по-різному.

І ось нам, як розробникам фреймворку, доводиться робити вибір на користь одного або іншого рішення. Або ж потрібно визнати, що жодне з них не є задовільним і тому потрібно придумувати щось інше. Такі рішення за час історії SObjectizer-а доводилося приймати неодноразово. Якщо комусь цікаво відчути себе в шкурі розробника такого фреймворку, то ласкаво прошу під кат.

Вихідна проблема
Отже, коротко суть вихідної проблеми. У SObjectizer, з самого початку його існування, була наступна особливість: таймерное повідомлення не так-то легко скасувати. Під таймерних далі буде розумітися, в першу чергу, відкладене повідомлення. Тобто повідомлення, яке не відразу повинно бути надіслано одержувачу, а через якийсь час. Наприклад, ми робимо send_delayed з паузою в 1s. Це говорить про те, що реально повідомлення буде відіслано по таймеру через 1s після виклику send_delayed.

Відкладене повідомлення, в принципі, можна скасувати. Якщо повідомлення все ще знаходиться у володінні таймера, то повідомлення після скасування нікуди не піде. Воно буде викинуто таймером і все. Але от якщо таймер вже відіслав повідомлення і воно зараз знаходиться в черзі заявок для агента-одержувача, то скасування таймера не спрацює. Немає у SObjectizer-е механізму вилучити повідомлення з черги заявок.

Проблема посилюється, як мінімум, двома факторами.

По-перше, в SObjectizer-е підтримується доставка в режимі 1:N, тобто якщо повідомлення було надіслано Multi-Consumer mbox, то повідомлення буде стояти в одній черзі, а відразу в кількох чергах для N одержувачів.

По-друге, в SObjectizer-е використовується механізм диспетчерів і диспетчери можуть бути самими різними, в тому числі і написаними користувачем під свої специфічні потреби. Чергами заявок керують диспетчери. І в інтерфейсі диспетчера не закладена функціональність вилучення заявки, яка вже була передана диспетчеру. Але навіть якщо така функціональність в інтерфейс була б закладена, то далеко не факт, що її можна було б реалізувати ефективно у всіх випадках. Не кажучи вже про те, що така функціональність б збільшила трудомісткість розробки нових диспетчерів.

Загалом, об’єктивно, якщо таймер вже зробив відсилання відкладеного повідомлення одержувачу(ям), то змусити SObjectizer не доставляти цей примірник повідомлення, на даний момент неможливо.
Насправді ця проблема актуальна і для періодичних повідомлень (тобто повідомлень, які таймер повинен відсилати періодично через задані інтервали часу). Але на практиці скасування періодичних повідомлень потрібна набагато рідше, ніж скасування відкладеного повідомлення. Принаймні в нашій практиці це так.

Що можна зробити прямо зараз?

Отже, проблема ця не нова і вже давно існують рекомендації про те, як з нею можна боротися.

Унікальний id всередині відкладеного повідомлення

Найпростіший спосіб — це ведення лічильника. У агента є лічильник, при відсиланні відкладеного повідомлення у повідомленні надсилається поточне значення лічильника. При скасуванні повідомлення лічильник у агента инкрементируется. При отриманні повідомлення порівнюється поточне значення лічильника в агента зі значенням з повідомлення. Якщо значення не збігаються, то повідомлення відкидається:

class demo_agent : public so_5::agent_t {
 struct delayed_msg final {
 int id_;
...
};

 int expected_msg_id_{};
 so_5::timer_id_t timer_;

 void on_some_event() {
 // Відсилаємо відкладене повідомлення.
 // Робимо виклик send_periodic, оскільки тільки ця функція
 // повертає timer_id для подальшої відміни.
 timer_ = so_5::send_periodic<delayed_msg>(*this,
 25s, // Через скільки повідомлення повинно прийти.
 0s, // Повторювати повідомлення не потрібно.
 // Далі йдуть параметри для конструктора delayed_msg,
 // першим з яких буде унікальний id для цього повідомлення.
++expected_msg_id_,
 ... // Інші значення.
);
...
}

 void on_cancel_event() {
 // Тут ми розуміємо, що відкладене повідомлення нам більше не потрібно
 // і відхиляємо його. В два кроки:
 timer_.reset(); // Таймер не буде обробляти повідомлення.
 ++expected_msg_id_; // Забезпечуємо розбіжність id-шників.
...
}

 void on_delayed_msg(mhood_t<delayed_msg> cmd) {
 // Обробляємо повідомлення тільки якщо id за минулий час
 // не змінився.
 if(expected_msg_id_ == cmd->id_) {
 ... // Обробка повідомлення.
}
}
};

Проблема цього способу в тому, що розробнику агента потрібно спантеличиться веденням цих лічильників. А якщо в якості відкладеного повідомлення нам потрібно відсилати чуже повідомлення, яке робив хтось інший, і в якому немає поля id_, то ми опиняємося в непростій ситуації.

Хоча, з іншого боку, це найефективніший спосіб з існуючих на даний момент.

Використовувати унікальний mbox для відкладеного повідомлення

Ще один спосіб, який добре працює, — це використання унікального поштової скриньки (mbox-а) для відкладеного повідомлення. У цьому випадку ми створюємо новий mbox для кожного відкладеного повідомлення, підписуємося на нього і відсилаємо відкладене повідомлення в mbox цей. Коли повідомлення потрібно скасувати, то ми просто видаляємо підписки на mbox.

class demo_agent : public so_5::agent_t {
 struct delayed_msg final {
 ... // Тут поле id_ вже не потрібно.
};

 so_5::mbox_t timer_mbox_; // Куди відправляємо повідомлення.
 so_5::timer_id_t timer_;

 void on_some_event() {
 // Для відкладеного надсилання повідомлення нам потрібен новий mbox
 // і створені для нього підписки.
 timer_mbox_ = so_environment().create_mbox();
 some_state.event(time_mbox_, ...);
 another_state.event(time_mbox_, ...);
...
 // Тепер можна відіслати повідомлення.
 timer_ = so_5::send_delayed<delayed_msg>(
so_environment(),
 timer_mbox_, // Обов'язково вказуємо куди йде повідомлення.
25s,
0s,
 ... // Параметри для конструктора delayed_msg.
);
}

 void on_cancel_event() {
 // Скасовуємо таймер і прибираємо підписки на тимчасовий mbox.
timer_.reset();
so_drop_subscription_for_all_states(timer_mbox_);
}

 void on_delayed_msg(mhood_t<delayed_msg> cmd) {
 // Тут просто обробляємо повідомлення знаючи, що це
 // повідомлення не було скасовано.
...
}
};

Цей спосіб вже може працювати з чужими повідомленнями, всередині яких немає ніякого унікального ідентифікатора. Але він так само вимагає зусиль і уваги від розробника.

Читайте також  Віконце з кнопками на JavaFX:

Наприклад, у наведеному вище варіанті немає захисту від того, що одне відкладене повідомлення вже було надіслано раніше. По-хорошому, перед відсиланням нового відкладеного повідомлення треба завжди виконувати дії з on_cancel_event(), інакше у агента будуть залишатися непотрібні йому підписки.

Чому ця проблема не була вирішена раніше?

Тут все досить просто: насправді це не така серйозна проблема, як може здатися. Принаймні, в реальному житті з нею стикатися доводиться не часто. Зазвичай відстрочені та періодичні повідомлення не скасовуються взагалі (саме тому, до речі кажучи, функція send_delayed не повертає timer_id). А коли необхідність у скасуванні виникає, то можна скористатися одним з описаних вище способів. Або навіть використовувати якийсь інший. Наприклад, створювати окремих агентів, які будуть обробляти відкладене повідомлення. Цих агентів можна скасувати реєстрацію коли відкладене повідомлення потрібно скасувати.

Так що на тлі інших завдань, які стояли перед нами, спрощення гарантованої скасування відкладеного повідомлення не було пріоритетним настільки, щоб витрачати свої ресурси на вирішення цієї проблеми.

Чому проблема стала актуальною саме зараз?

Тут так само все просто. З одного боку, нарешті дійшли руки.

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

Крім того, нас були свої завдання, нам не потрібно було постійно скасовувати відкладені повідомлення. А у нових користувачів свої завдання, може там все навпаки.

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

Час від часу ця можливість виявляється затребуваною. Наприклад, уявімо собі, що у нас є кілька взаємодіючих агентів двох типів: entry_point (приймає запити від клієнтів), і processor (обробляє запити):

Агенти entry_point надсилають запити агенту processor, той їх по мірі сил обробляє і відповідає агентам entry_point. Але часом entry_point може виявити, що обробка раніше відісланого запиту більше не потрібна. Наприклад, клієнт надіслав команду cancel або ж клієнт «відвалився» і обробляти його запити вже не потрібно. Зараз, якщо повідомлення request стоять у черзі агента processor, то відкликати їх не можна. А було б корисно.

Тому нинішній візит до вирішення проблеми «гарантованої скасування таймера» виконується як додавання підтримки «відзивних повідомлень». Спеціальним чином відсилаємо будь-яке повідомлення, отримуємо на руки якийсь хендл, з допомогою якого потім повідомлення можна відкликати. І вже не настільки принципово, висловлюється чи звичайне повідомлення або відкладене.

Спроба придумати реалізацію «відзивних повідомлень»
Отже, треба ввести поняття «відкличного повідомлення» і підтримати це поняття в SObjectizer. Причому так, щоб залишитися в рамках гілки 5.5. Перша версія з цієї гілки, 5.5.0, вийшла практично чотири роки тому, в жовтні 2014-го. З тих пір якихось серйозних ламають змін до 5.5 не було. Проекти, які вже перейшли або ж відразу стартували на SObjectize-5.5 можуть переходити на нові релізи в гілці 5.5 без будь-яких проблем. Таку сумісність треба зберегти і в цей раз.

Загалом, все просто: потрібно взяти і зробити.

Що зрозуміло як робити

Після першого підходу до проблеми стали зрозумілі дві речі з приводу реалізації «відзивних повідомлень».

Атомарний прапор і його перевірка перед обробкою повідомлення

По-перше, очевидно, що в рамках поточної архітектури SObjectizer-5.5 (а може і більш глобально: в рамках принципів роботи самого SObjectizer-5) не можна вилучати повідомлення з черг заявок диспетчерів, де повідомлення чекають, поки агенти-одержувачі опрацюють їх. Спроба зробити це вб’є всю ідею різнорідних диспетчерів, які навіть користувач може робити власні, під специфіку свого завдання (наприклад, ось такий). Крім того, у разі розсилання повідомлення в режимі 1:N, де N буде великим, дорого буде зберігати список покажчиків на примірник відісланого повідомлення у всіх чергах.

Це означає, що разом з повідомленням повинен передаватися якийсь атомарний прапорець, який потрібно буде аналізувати відразу після того, як повідомлення витягується з черги заявок, але перед тим, як повідомлення передається на обробку агенту-одержувачу. Тобто повідомлення потрапляє в чергу і нікуди звідти вона вже не вилучається. Але коли до повідомлення доходить чергу, перевіряється його прапор. І якщо прапор каже, що повідомлення відкликано, то обробка повідомлення не виконується.

Відповідно, сам відгук повідомлення полягає у виставленні спеціального значення для атомарного прапора усередині повідомлення.

Об’єкт revocable_handle_t<M>

По-друге, поки що(?) очевидно, що для відсилання відкличного повідомлення повинні застосовуватися не звичайні методи відсилання повідомлень, а спеціальний об’єкт під умовною назвою revocable_handle_t.

Для того, щоб надіслати здатний повідомлення користувач повинен створити екземпляр revocable_handle_t, після чого викликати у цього примірника метод send. А якщо повідомлення потрібно відкликати, то це робиться за допомогою методу revoke. Щось на зразок:

struct my_message {...};
...
so_5::revocable_handle_t<my_message> msg;
// Конструюємо і відсилаємо повідомлення.
msg.send(target, // Куди надсилається.
 ... // Параметри для конструктора my_message.
);
...
// Захотіли відкликати повідомлення.
msg.revoke();

Чітких деталей реалізації revocable_handle_t поки немає, що не дивно, оскільки сам механізм роботи відзивних повідомлень поки ще не вибрано. Але принцип роботи полягає в тому, що в revocable_handle_t зберігається розумна посилання на відправлене повідомлення на атомарний прапор для нього. У методі revoke() відбувається спроба замінити значення прапора. Якщо це вдається, то повідомлення після вилучення з черги заявок, піддаватися обробці вже не буде.

З чим це не буде дружити

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

Читайте також  Пасхалка-текстова RPG в коді пошуковика Google

message_limits

Така важлива фіча SObjectizer-а, як message_limits, призначена для захисту агентів від перевантаження. Працюють message_limits на основі підрахунку повідомлень в черзі. Поставили повідомлення в чергу — збільшили лічильник. Дістали з черги — зменшили.

Т. к. при відкликання повідомлення, воно продовжує залишатися в черзі, то на message_limits відгук повідомлення впливу не надає. Тому може вийти так, що в черзі стоїть гранична кількість повідомлень типу M, але всі вони відкликані. По факту жодне з них не буде оброблено. Але поставити нове повідомлення типу M в чергу не вийде, т. к. відбудеться перевищення ліміту.

Ситуація кепська. Але як з неї вийти? Незрозуміло.

mchain-и з фіксованим розміром черги

У SObjectizer повідомлення можна відправити не тільки в mbox, але і в mchain (це наш аналог CSP-ного каналу). А mchain-и можуть мати фіксований розмір своїх черг. Спроба помістити в повний mchain нове повідомлення для mchain-а з фіксованим розміром повинна привести до якоїсь реакції. Наприклад, на очікування звільнення місця в черзі. Або до виштовхування самого старого повідомлення.

У випадку ж з відкликанням повідомлення, воно залишиться всередині черги mchain-а. Вийде, що повідомлення вже не потрібно, але воно в черзі mchain-а займає. І перешкоджає відсилання в mchain нових повідомлень.

Така ж негарна ситуація, як і у випадку з message_limits. І знову незрозуміло, як її можна виправити.

Що незрозуміло як робити

Ось ми і підібралися до вибору між двома (поки що?) варіантами реалізації відзивних повідомлень. Перший варіант простий в реалізації і не вимагає переробки потрухів SObjectizer-а. Другий варіант набагато складніше, але зате в ньому одержувач повідомлення навіть не знає, що має справу з відзивними повідомленнями. Коротко розглянемо кожен з них.

Отримання відзивних повідомлень у вигляді revocable_t<M>

Перше рішення, яке виглядає, по-перше, реалізованим і, по-друге, досить практичним, — це введення спеціальної обгортки revocable_t<M>. Коли користувач відсилає здатний повідомлення типу M через revocable_handle_t<M>, то відсилається не саме повідомлення M, а повідомлення M всередині спеціальної обгортки revocable_t<M>. І, відповідно, отримувати та обробляти користувач буде не повідомлення типу M, а повідомлення revocable_t<M>. Наприклад, таким чином:

class processor : public so_5::agent_t {
public:
 struct request { ... }; // Повідомлення, яке може бути відкликано.

 void so_define_agent() override {
 // Підписуємося на повідомлення.
so_subscribe_self().event(
 // Ось ця сигнатура явно показує, що ми працюємо
 // з відзивним повідомленням.
 [this](mhood_t< revocable_t<request> > cmd) {
 // Обробляємо, але тільки якщо повідомлення не відкликали.
 cmd->try_handle([this](mhood_t<request> msg) {
...
});
});
...
}
...
};

Метод revocable_t<M>::try_handle() перевіряє значення атомарного прапора і, якщо повідомлення не відкликано, викликає передану йому лямбда-функцію. Якщо ж повідомлення відкликано, то try_handle() нічого не робить.

Плюси і мінуси цього підходу

Головний плюс в тому, що цей похід легко реалізується (принаймні поки так видається). Фактично, revocable_handle_t<M> і revocable_t<M> всього лише тонкою надбудовою над SObjectizer-му.

Втручання у нутрощі SObjectizer-а може знадобитися для того, щоб подружити revocable_t і mutable_msg. Справа в тому, що в SObjectizer є поняття иммутабельных повідомлень (вони можуть відсилатися як в режимі 1:1, так і в режимі 1:N). І є поняття мутабельных повідомлень, які можуть надсилатися тільки в режимі 1:1. При цьому SObjectizer спеціальним чином трактує маркер mutable_msg<M> і виконує відповідні перевірки в run-time. У випадку з revocable_t<mutable_msg<M>> потрібно буде навчити SObjectizer трактувати цю конструкцію як mutable_msg<M>.

Ще один плюс в тому, що додаткові накладні витрати (як на метадані відкличного повідомлення, так на перевірку атомарного прапора), будуть тільки в тих місцях, де без цього не обійтися. Там же, де відкличні повідомлення не використовуються, ніяких додаткових накладних витрат не буде взагалі.

Ну а головний мінус ідеологічний. У цьому підході факт використання відзивних повідомлень позначається як на відправника (використання revocable_handle_t<M>), так і на одержувача (використання revocable_t<M>). А от як раз одержувачу-то й нема чого знати, що він отримує відкличні повідомлення. Тим більше, що в якості одержувача у вас може бути вже готовий сторонній агент, який написаний без revocable_t<M>.

Крім того, тут залишаються ідеологічні питання про, наприклад, можливості перепосылки таких повідомлень. Але, за першими підрахунками, ці питання вирішуються.

Отримання відзивних повідомлень у вигляді звичайних повідомлень

Другий підхід полягає в тому, щоб на стороні одержувача бачити тільки повідомлення типу M і не мати уявлення про існування revocable_handle_t<M> і revocable_t<M>. Тобто якщо processor повинен отримувати request, то він і повинен бачити тільки request, без якихось додаткових обгорток.

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

Плюси і мінуси цього підходу

Головний плюс даного підходу очевидна — одержувач повідомлення не знає, з якими повідомленнями він працює. Що дозволяє відправнику повідомлень спокійно відкликати повідомлення для будь-яких агентів, навіть тих, які були написані іншими розробниками.

Ще один важливий плюс — це можливість інтеграції з механізмом message delivery tracing (тут роль цього механізму описана детальніше). Тобто якщо msg_tracing включений і відправник відкликає повідомлення, то сліди цього можна буде відшукати в балці msg_tracing-а. Що дуже зручно при налагодженні.

А ось головним мінусом є трудомісткість реалізації цього підходу. При якій потрібно буде враховувати кілька факторів.

По-перше, накладні витрати. Різного роду.

Скажімо, можна зробити спеціальний прапор всередині повідомлення, який буде вказувати здатний це повідомлення чи ні. А потім перевіряти цей прапор перед початком обробки кожного повідомлення. Грубо кажучи, механізм доставки повідомлень додається ще один if, який буде відпрацьовувати при обробці кожного(!) повідомлення.

Я впевнений, що в реальних додатках втрати на цей if будуть ледве помітні. Але ось просадка в синтетичних бенчмарках обов’язково проявиться. Причому, чим абстрактней бенчмарк, чим менше реальної роботи він робить, тим сильніше просяде. І це погано з маркетингової точки зору, оскільки є деяка кількість індивідів, які роблять висновки про фреймворку за показниками синтетичних бенчмарків. Причому роблять це специфічно: не розбираючись з тим, що це за бенчмарк, що він в принципі показує, на якому залозі він працює, але порівнюючи підсумкові цифри з показниками якогось спеціалізованого інструменту, на іншому сценарії, на іншому залозі і т. д. і т. п.

Читайте також  Готуємо Matrix в домашніх умовах

Загалом, оскільки ми робимо універсальний фреймворк, про який, так вже виходить, судять за абстрактним цифр в абстрактних бенчмарках, то не хочеться втрачати, скажімо, 5% продуктивності в механізмі доставки всіх повідомлень через додавання фічі, яка потрібна буде лише час від часу, і то не всім користувачам.

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

struct execution_demand_t
{
 //! Receiver of demand.
 agent_t * m_receiver;
 //! Optional message limit for that message.
 const message_limit::control_block_t * m_limit;
 //! ID of mbox.
 mbox_id_t m_mbox_id;
 //! Type of the message.
 std::type_index m_msg_type;
 //! Event incident.
 message_ref_t m_message_ref;
 //! Demand handler.
 demand_handler_pfn_t m_demand_handler;
...
};

Де demand_handler_pfn_t — це звичайний вказівник на функцію:

typedef void (*demand_handler_pfn_t)(
current_thread_id_t,
 execution_demand_t & );

Цей же механізм можна використовувати і для того, щоб спеціальним чином обробляти склав повідомлення. Тобто коли mbox віддає агенту повідомлення, то агент знає, чи надається йому асинхронне повідомлення або синхронний запит. Точно так само агенту можна спеціальним чином віддавати асинхронне склав повідомлення. І агент збереже разом з повідомленням вказівник на функцію, яка знає, як повинно обробляти відкличні повідомлення.

Начебто все добре, але є два великих «але»… 🙁

По-перше, існуючий інтерфейс mbox-ов (а саме клас abstract_message_mbox_t) не має методів для відсилання відзивних повідомлень. Значить цей інтерфейс потрібно розширювати. Причому так, щоб не поламалися чужі реалізації mbox-ів, які зав’язані на abstract_message_box_t з SObjectizer-5.5 (зокрема, ряд mbox реалізований в so_5_extra і ламати їх просто так не хочеться).

По-друге, повідомлення можуть надсилатися не тільки в mbox-и, за якими заховані агенти, але і в mchain-и. Які є нашими аналогами CSP-шних каналів. А там досі заявки лежали без яких-небудь додаткових вказівників на функції. Вводити в кожен елемент черги заявок mchain-а додатковий покажчик… Можна, звичайно, але виглядає досить дорогим рішенням. Крім того, самі реалізації mchain-ів поки що не передбачали ситуації, при якій витягнуте повідомлення потрібно перевірити і, можливо, викинути.

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

А що ж з гарантованою скасуванням відкладених повідомлень?

Боюся, в нетрях технічних деталей загубилася вихідна проблема. Припустимо, відкличні повідомлення є, як же буде відбуватися скасування відкладених/періодичних повідомлень?

Тут, як мовиться, можливі варіанти. Наприклад, робота з відкладеними/періодичними повідомленнями може бути частиною функціональності revocable_handle_t<M>:

revocable_handle_t<my_mesage> msg;
msg.send_delayed(target, 15s, ...);
...
msg.revoke();

Або ж можна буде зробити поверх revocable_handle_t<M> додатковий допоміжний клас cancelable_timer_t<M>, який і буде надавати методи send_delayed/send_periodic.

Біла пляма: синхронні запити

SObjectizer-5 підтримує не тільки асинхронне взаємодія між сутностями в програмі (через надсилання повідомлення в mbox-и і mchain-и), але і синхронне взаємодія через request_value/request_future. Це синхронна взаємодія працює не тільки для агентів. Тобто можна не тільки відіслати синхронний запит агенту через його mbox. У випадку з mchain-ами так само можна робити синхронні запити, наприклад, до іншої робочої нитки, на якій викликали receive() або select() для mchain-а.

Так от, поки ще незрозуміло, чи варто дозволяти використовувати синхронні запити спільно з відзивними повідомленнями. З одного боку, може бути в цьому і є якийсь сенс. І виглядати це може, наприклад, ось так:

revocable_handle_t<my_request> msg;
auto f = msg.request_future<my_reply>(target, ...);
...
if(some_condition)
msg.revoke();
...
f.get(); // Отримаємо виняток у разі попереднього revoke().

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

Вибирай, але обережно. Але вибирай
Отже, є розуміння проблеми. Є два варіанти її вирішення. Які на даний момент здаються реалізованими. Але сильно відрізняються рівнем зручності, що надається користувачеві, а ще сильніше вони відрізняються вартістю реалізації.

Між цими двома варіантами потрібно вибрати. Або ж придумати щось інше.

У чому складність вибору?

Складність в тому, що SObjectizer — це безкоштовний фреймворк. Грошей він нам безпосередньо не приносить. Ми його робимо, що називається, за свої. Тому чисто з економічних переваг більш простий і швидкий у реалізації варіант вигідніше.

Але, з іншого боку, не все вимірюється грошима і в довгостроковій перспективі якісно зроблений інструмент, фічі якого нормально пов’язані один з одним, краще, ніж клаптева ковдра з налепленных абияк латочок. Якість оцінюють як користувачі, так і ми самі, коли надалі супроводжуємо свою розробку і додаємо в неї нові фічі.

Так що вибір, по суті, йде між миттєвою вигодою та довгостроковими перспективами. Правда, в сучасному світі у C++ інструментів з довгостроковими перспективами якось туманно. Що робить вибір ще більш складним.

Ось в таких умовах і доводиться вибирати. Обережно. Але вибирати.

Висновок
У даній статті ми спробували трохи показати процес проектування і впровадження нових фіч в наш фреймворк. Такий процес відбувається у нас регулярно. Раніше частіше, т. к. в 2014-2016рр SObjectizer розвивався набагато активніше. Зараз темпи випуску нових версії знизилися. Що об’єктивно, в тому числі й тому, що додавати нову функціональність нічого не поламавши, з кожною новою версією стає складніше.

Сподіваюся, було цікаво заглянути до нас за лаштунки. Спасибі за увагу!

Степан Лютий

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

You may also like...

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

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