SObjectizer -- это один из немногих все еще живых и развивающихся "акторных фреймворков" для C++ (еще есть QP/C++, CAF: C++ Actor Framework и совсем молодой еще проект rotor). Краткий обзор SObjectizer-а можно найти в этой презентации или в этой довольно старой уже статье. Хотелось бы подчеркнуть, что SObjectizer поддерживает не только модель акторов, но еще и такие модели как Publish-Subscribe и Communicating Sequential Processes. А so5extra – это набор дополнительных полезных прибамбасов для SObjectizer-а (например, реализованные на базе Asio диспетчер с пулом нитей и env_infrastructures, дополнительные типы message box-ов, средства для реализации синхронного взаимодействия и т.д.)
Если в двух словах, то:
* SObjectizer-5.7 теперь позволяет использовать `send_case` в функции `select()`. Это делает SObjectizer-овский `select()` гораздо более похожим на `select` из Golang-а. Но это нововведение нарушило совместимость с предыдущей версией 5.6, т.к. теперь старая функция `case_` стала называться `receive_case`;
* в версии 5.7 устранен недочет в механизме доставки обернутых в конверты сообщений (т.е. enveloped messages) в случае использования `transfer_to_state()` и `suppress()` у агентов-получателей;
* код so5extra теперь распространяется под BSD-3-CLAUSE лицензией, что позволяет бесплатно использовать so5extra при разработке закрытого программного обеспечения. Предыдущие версии распространялись под двойной лицензией (GNU Affero GPL v.3 и коммерческой);
* в so5extra-1.4 реализованы mchain-ы фиксированной емкости для случаев, когда эта емкость известна на этапе компиляции.
Если же рассказывать более подробно, то основная фишка SObjectizer-5.7 -- это возможность использования `select()` для отсылки исходящих сообщений (по аналогии с тем, как это происходит в Golang-е). Так что теперь можно делать вот такие вещи:
using namespace so_5;
struct Greetings {
std::string msg_;
};
// Попытка отослать сообщения в соответствующие каналы,
// но все операции должны уложиться в 250ms.
select(from_all().handle_n(3).total_time(250ms),
send_case(chAlice,
message_holder_t<Greetings>::make("Hello, Alice!"),
[]{ std::cout << "message sent to chAlice" << std::endl; }),
send_case(chBob,
message_holder_t<Greetings>::make("Hello, Bob!"),
[]{ std::cout << "message sent to chBob" << std::endl; }),
send_case(chEve,
message_holder_t<Greeting>::make("Hello, Eve!"),
[]{ std::cout << "message sent to chEve" << std::endl; }));
В одном `select()` можно использовать и `send_case()` и `receive_case()` вместе. Например, вот SObjectizer-овская версия вычисления чисел Фибоначчи из в отдельном рабочем потоке (по мотивам из Golang's tour):
using namespace std;
using namespace std::chrono_literals;
using namespace so_5;
struct quit {};
void fibonacci( mchain_t values_ch, mchain_t quit_ch )
{
int x = 0, y = 1;
mchain_select_result_t r;
do
{
r = select(
from_all().handle_n(1),
// Отсылка сообщения типа 'int' со значением 'x' внутри.
// Отсылка выполняется только когда values_ch готов для приема
// новых исходящих сообщений.
send_case( values_ch, message_holder_t<int>::make(x),
[&x, &y] { // This block of code will be called after the send().auto old_x = x;
x = y; y = old_x + y;
} ),
// Ожидание сообщения типа `quit` из канала quit_ch.
receive_case( quit_ch, [](quit){} ) );
}
// Продолжаем операции пока что-то отсылается и ничего не прочитано.while( r.was_sent() && !r.was_handled() );
}
int main()
{
wrapped_env_t sobj;
thread fibonacci_thr;
auto thr_joiner = auto_join( fibonacci_thr );
// Канал для чисел Фибоначчи будет иметь ограниченный объем.auto values_ch = create_mchain( sobj, 1s, 1,
mchain_props::memory_usage_t::preallocated,
mchain_props::overflow_reaction_t::abort_app );
auto quit_ch = create_mchain( sobj );
auto ch_closer = auto_close_drop_content( values_ch, quit_ch );
fibonacci_thr = thread{ fibonacci, values_ch, quit_ch };
// Читаем первые 10 значений из values_ch.
receive( from( values_ch ).handle_n( 10 ),
// Отображаем каждое прочитанное значение.
[]( int v ) { cout << v << endl; } );
send< quit >( quit_ch );
}
Полное описание нововведений версии 5.7.0 можно найти здесь.
Основное изменение в so5extra-1.4 -- это смена лицензии на BSD-3-CLAUSE. Поэтому теперь все множество дополнений к SObjectizer-у из so5extra могут бесплатно использоваться в разработке закрытого коммерческого ПО.
Единственное нововведение в so5extra-1.4 -- это реализация mchain для случая, когда максимальный объем mchain-а известен на этапе компиляции. Подобные mchain-ы зачастую используются в сценариях request-response, где ожидается всего одно ответное сообщение на запрос:
#include <so_5_extra/mchains/fixed_size.hpp>
#include <so_5/all.hpp>
...
using namespace so_5;
// Канал для получения ответного сообщения.auto reply_ch = extra::mchains::fixed_size::create_mchain<1>(env,
mchain_props::overflow_reaction_t::drop_newset);
// Отсылаем запрос.
send<SomeRequest>(target, ..., reply_ch, ...);
// Ждем и обрабатываем ответ.
receive(so_5::from(reply_ch).handle_n(1), [](const SomeReply & reply) { ... });
Надеюсь, что SObjectizer/so5extra кому-нибудь окажется полезен. Если есть вопросы, то спрашивайте, постараюсь ответить.
PS. Изначально SObjectizer/so5extra жили на SourceForge, потом перехали на BitBucket, но в связи с тем, что вскоре с BitBucket-а окончательно выпилят все Mercurial-репозитории, SO-5/so5extra теперь уже живут и развиваются только на GitHub-е.
Полный список изменений в SObjectizer-5.7.1 можно найти здесь.
Из новых возможностей отдельно можно выделить лимиты для сообщений по умолчанию. Начиная с 5.7.1 если агенту нужно обрабатывать множество сообщений с одинаковыми лимитами, то описание этого лимита можно сделать лишь один раз:
class demo final : public so_5::agent_t
{
public:
demo(context_t ctx)
: so_5::agent_t{ctx
+ limit_then_drop<msg_A>(100u)
+ limit_then_abort<msg_B>(10u)
// Этот лимит будет использован для всех остальных сообщений.
+ limit_then_drop<any_unspecified_message>(50u)
}
{}
void so_define_agent() override
{
// Для сообщения msg_A будет использован явно заданный лимит.
so_subscribe_self().event([](mhood_t<msg_A> cmd) {...});
// Для сообщения msg_C лимит будет создан автоматически.
so_subscribe_self().event([](mhood_t<msg_C> cmd) {...});
}
};
В so5extra-1.4.1 добавлен новый диспетчер asio_one_thread. Этот диспетчер предназначен для ситуаций, когда на одной рабочей нити нужно собрать сразу несколько агентов, которые выполняют IO-операции посредством Asio. И когда важно, чтобы все эти IO-операции выполнялись на одной и той же рабочей нити.
Наконец дошли руки до SObjectizer для своего пет-проекта. Пока полёт отличный, но изучение идёт не так быстро, как хотелось бы. Но чем дальше, тем лучше. Мой технический стек на данный момент: Qt, SObjectizer, sqlite_orm, grpc, boost.
Из мелких пожеланий — выложить куда-нибудь онлайн сгенерированную документацию, чтобы самому не возиться с doxygen. Так же можно больше примеров в стиле best practice
Спасибо за вашу работу. Если замечу баги, то буду спрашивать на гитхабе или тут.
Здравствуйте, SaZ, Вы писали:
SaZ>Наконец дошли руки до SObjectizer для своего пет-проекта. Пока полёт отличный, но изучение идёт не так быстро, как хотелось бы. Но чем дальше, тем лучше.
Для изучения еще можно использовать нашу старую Wiki на SourceForge: https://sourceforge.net/p/sobjectizer/wiki/Basics/
Там есть целая серия статей SO-5.5 By Example. Правда, для ветки 5.5, для 5.6/5.7 не было ресурсов пока эту серию обновить и перенести на github.
SaZ>Из мелких пожеланий — выложить куда-нибудь онлайн сгенерированную документацию, чтобы самому не возиться с doxygen.
struct Proceed final: public so_5::signal_t{};
//...
so_5::send<Proceed>(*this); // this - агент
Получаю статический ассерт: message.hpp(543): error C2338: message class must be derived from the message_t.
Как правильно писать обработчики сигналов?
Здравствуйте, SaZ, Вы писали:
SaZ>Пытаюсь отправить сигнал сам себе, что-то вроде SaZ>
SaZ>struct Proceed final: public so_5::signal_t{};
SaZ>//...
SaZ>so_5::send<Proceed>(*this); // this - агент
SaZ>
SaZ>Получаю статический ассерт: message.hpp(543): error C2338: message class must be derived from the message_t. SaZ>Как правильно писать обработчики сигналов?
А ошибка диагностируется в месте отсылки сигнала или в месте подписки на сигнал?
Здравствуйте, so5team, Вы писали:
S>Здравствуйте, SaZ, Вы писали:
SaZ>>Пытаюсь отправить сигнал сам себе, что-то вроде SaZ>>
SaZ>>struct Proceed final: public so_5::signal_t{};
SaZ>>//...
SaZ>>so_5::send<Proceed>(*this); // this - агент
SaZ>>
SaZ>>Получаю статический ассерт: message.hpp(543): error C2338: message class must be derived from the message_t. SaZ>>Как правильно писать обработчики сигналов?
S>А ошибка диагностируется в месте отсылки сигнала или в месте подписки на сигнал?
Диагностируется в месте отсылки, но пропадает, если я удаляю подписку на обработчик. Поэтому я подразумеваю, что что-то не так с обработчиком.
class myagent: ...
{
void onSignal(const Proceed&); // при void onSignal(Proceed); чуть другая ошибка
...
so_subscribe_self().event(&myagent::onSignal); // Если закомментировать, то ошибка пропадает, но сигнал идёт вникуда.
}
Если не разберёмся сейчас, то накидаю минимальный примерчик.
Здравствуйте, SaZ, Вы писали:
SaZ>Диагностируется в месте отсылки, но пропадает, если я удаляю подписку на обработчик. Поэтому я подразумеваю, что что-то не так с обработчиком. SaZ>
SaZ>class myagent: ...
SaZ>{
SaZ> void onSignal(const Proceed&); // при void onSignal(Proceed); чуть другая ошибка
SaZ>...
SaZ> so_subscribe_self().event(&myagent::onSignal); // Если закомментировать, то ошибка пропадает, но сигнал идёт вникуда.
SaZ>}
SaZ>
Тогда все правильно. Сигналы не переносят информации, поэтому нет экземпляра Proceed и, соответственно, нельзя иметь обработчик сигнала в формате void(const Proceed&). Обработчик для сигнала должен иметь формат void(mhood_t<Proceed>).
Здравствуйте, so5team, Вы писали:
S>Тогда все правильно. Сигналы не переносят информации, поэтому нет экземпляра Proceed и, соответственно, нельзя иметь обработчик сигнала в формате void(const Proceed&). Обработчик для сигнала должен иметь формат void(mhood_t<Proceed>).
Благодарю, помогло. Я ещё почитаю документацию/хабр, пока понимаю, что фундаментальных знаний не хватает.
Сейчас пытаюсь написать простейший микросервис и скрестить gRPC и SObjectizer.
SObjectizer-5, анонсы релизов которого здесь время от времени публикуются, недавно исполнилось 10 лет.
Не смотря на то, что в последние года 2-3 темп развития SObjectizer-5 снизился, проект не заброшен, он живет. Недавно был выпущен корректирующий релиз для ветки 5.7.
Для тех, кто сталкивается с отсутствием приоритеной обработки сообщений в SObjectizer, может быть интересна эта статья. В ней рассказывается, как сделать доставку сообщений с учетом их приоритетов своими руками. Ну а если кто-то хочет видеть подобную функциональность сразу в SObjectizer-е, то дайте нам знать, пожалуйста. Можно будет предметно пообщаться и сделать то, что действительно кому-то нужно.
Re: Как выглядит код на SObjectizer в реальном проекте?
Мы рассказываем о SObjectizer уже давно. И сам SObjectizer снабжен большим количеством примеров.
Но вот о том, как выглядит реальный код, написанный для продакшена, у нас пока рассказать возможности не было.
Так что если кто-то задавался вопросом тянуть ли к себе в проект SObjectizer (и/или RESTinio), то теперь можно посмотреть и ужаснуться сделать собственные выводы.
Принципиально нового в этих релизах нет, всего лишь представилась возможность реализовать некоторые накопившиеся по мере использования SObjectizer-а хотелки.
Подробнее об изменениях в SObjectizer можно прочитать здесь, а об изменениях в so5extra -- здесь.
В SObjectizer-5.7.3 хотелось бы выделить две новые фичи.
Во-первых, это возможность использовать собственные рабочие нити со штатными диспетчерами. Ранее штатные диспетчеры сами создавали экземпляры std::thread и как-то повлиять на это было нельзя. Теперь же можно сделать собственную фабрику рабочих нитей, к которой SObjectizer будет обращаться когда диспетчерам потребуются новые рабочие нити. Подробнее здесь.
Во-вторых, это новый метод `agent_t::so_deactivate_agent`, который переводит агента в специальное неактивное состояние и отменяет все подписки агента. Это может потребоваться когда в каком-то агенте возникла непоправимая ошибка и этот агент должен выключится из работы до тех пор, пока не будет дерегистрирована кооперация с этим агентом.
В so5extra-1.5.0 добавлен новый тип mbox-а: unique-subscribers. С одной стороны он похож на обычный Multi-Producer/Multi-Consumer mbox: сразу несколько агентов могут одновременно подписаться на него. Но, это Multi-Producer/Single-Consumer mbox. Все подписчики должны подписываться на разные типы сообщений. Это позволяет использовать unique-subscribers для публикации мутабельных сообщений не зная при этом, кто именно будет обрабатывать опубликованное сообщение. На обычных MPSC mbox-ах такого не сделать, т.к. при отсылке сообщения нужно знать конкретный MPSC mbox конкретного обработчика, а это не всегда удобно. Подробнее здесь.
Здравствуйте, so5team, Вы писали:
S>Вышли очередные версии библиотек SObjectizer и so5extra.
>...
Есть ли пример подтягивания SObjectizer через FetchContent из cmake?
Был бы весьма благодарен, если бы кто-то поделился, если таковой пример существует.
Здравствуйте, A13x, Вы писали:
A>Есть ли пример подтягивания SObjectizer через FetchContent из cmake? A>Был бы весьма благодарен, если бы кто-то поделился, если таковой пример существует.
Но там есть засада, с которой пока не понятно что делать: если компилировать SObjectizer как SharedLib, то .so-шка затем не копируется куда следует при выполнении cmake --build . --target install. Как это победить непонятно, если у кого-то есть соображения или подсказки, то поделитесь, плз, это может помочь.
Здорово, спасибо большое! Мне вполне достаточно статической версии, так что пример вполне подходит. Можно было бы немножко упростить инструкцию по сборке убрав ключи, которые, наверное, больше были бы важны для кастомной сборки, во всяком случае "стандартная" последовательность работает:
mkdir build
cd build
cmake ..
make
./hello_world/sample.so_5.hello_world
Недавно удалось добавить в сопутствующий проект so5extra еще несколько новых реализаций mbox-ов (почтовых ящиков) и зафиксировать очередную версию so5extra-1.5.2.
Взять ее можно с GitHub или же воспользоваться vcpkg или conan.
Для лучшего понимания того, что из себя представляет so5extra и что там есть была подготовлена обзорная статья.
Менее полный, но с большим погружением в отдельные моменты, обзор изменений сделан в свежей статье на Хабре.
Если же говорить в двух словах, то начата новая ветка 5.8, в которой пришлось пойти на слом совместимости с предыдущей веткой 5.7. Так что при переходе на SO-5.8 потребуется модифицировать исходники. Не получилось придумать способ внедрить новую функциональность не поломав совместимость
Здравствуйте, so5team, Вы писали:
S>Если же говорить в двух словах, то начата новая ветка 5.8, в которой пришлось пойти на слом совместимости с предыдущей веткой 5.7. Так что при переходе на SO-5.8 потребуется модифицировать исходники. Не получилось придумать способ внедрить новую функциональность не поломав совместимость
А почему не соблюдаете semver? Это же всем удобней. Сломанная совместимость это признак новой мажорной версии.
Здравствуйте, vsb, Вы писали:
S>>Если же говорить в двух словах, то начата новая ветка 5.8, в которой пришлось пойти на слом совместимости с предыдущей веткой 5.7. Так что при переходе на SO-5.8 потребуется модифицировать исходники. Не получилось придумать способ внедрить новую функциональность не поломав совместимость
vsb>А почему не соблюдаете semver? Это же всем удобней. Сломанная совместимость это признак новой мажорной версии.
Сложный политкорректный ответ: мы разделяем потерю совместимости на два уровня:
1) версии библиотеки X настолько разные, что X_1 и X_2 можно использовать в одном исходном файле. Грубо говоря:
В нашем случае при такой потере совместимости меняется первая цифра. Т.е. был SObjectizer-4, стал SObjectizer-5. Может быть лет через 10-15 будет SObjectizer-6.
2) версии библиотеки X не настолько разные, чтобы X_1 и X_2 можно было использовать в одном исходном файле. Например, в версии X_2 удалили что-то или поменяли формат какого-то метода. И если программисту нужно перескакивать с X_1 на X_2 и обратно, то придется делать это через #if-ы (вроде такого).
В данном случае слом совместимости произошел на уровне 2), поэтому первая цифра не меняется.
Простой политкорректный ответ: т.к. проекты SObjectizer-4 и SObjectizer-5, по большому счету можно рассматривать как сильно разные, то первую цифру версии можно трактовать как часть имени проекта. Поэтому SObjectizer-5.8.0 можно рассматривать как SObjectizer-5 версии 8.0. Т.о. восьмерка становится первой цифрой версии и semver как бы соблюдается.
Неполиткорректный ответ: semver, может быть, хорошо работает для приложений, но (на мой субъективный взгляд) откровенно дерьмово для библиотек. Обсуждение этого выходит за рамки топика.