Четыре года развития SObjectizer-5.5. Как SObjectizer изменился за это время?

    Первая версия SObjectizer-а в рамках ветки 5.5 вышла чуть больше четырех лет назад — в начале октября 2014-го года. А сегодня увидела свет очередная версия под номером 5.5.23, которая, вполне возможно, закроет историю развития SObjectizer-5.5. По-моему, это отличный повод оглянуться назад и посмотреть, что же было сделано за минувшие четыре года.

    В этой статье я попробую тезисно разобрать наиболее важные и знаковые изменения и нововведения: что было добавлено, зачем, как это повлияло на сам SObjectizer или его использование.

    Возможно, кому-то такой рассказ будет интересен с точки зрения археологии. А кого-то, возможно, удержит от такого сомнительного приключения, как разработка собственного акторного фреймворка для C++ ;)

    Небольшое лирическое отступление про роль старых C++ компиляторов


    История SObjectizer-5 началась в середине 2010-го года. При этом мы сразу ориентировались на C++0x. Уже в 2011-ом первые версии SObjectizer-5 стали использоваться для написания production-кода. Понятное дело, что компиляторов с нормальной поддержкой C++11 у нас тогда не было.

    Долгое время мы не могли использовать в полной мере все возможности «современного C++»: variadic templates, noexcept, constexpr и пр. Это не могло не сказаться на API SObjectizer-а. И сказывалось еще очень и очень долго. Поэтому, если при чтении описания какой-то фичи у вас возникает вопрос «А почему так не было сделано раньше?», то ответ на такой вопрос скорее всего: «Потому, что раньше не было возможности».

    Что появилось и/или изменилось в SObjectizer-5.5 за прошедшее время?


    В данном разделе мы пройдемся по ряду фич, которые оказали существенное влияние на SObjectizer. Порядок следования в этом списке случаен и не имеет отношения к «значимости» или «весу» описываемых фич.

    Отказ от пространства имен so_5::rt


    Что было?


    Изначально в пятом SObjectizer-е все, что относилось к рантайму SObjectizer-а, определялось внутри пространства имен so_5::rt. Например, у нас были so_5::rt::environment_t, so_5::rt::agent_t, so_5::rt::message_t и т.д. Что можно увидеть, например, в традиционном примере HelloWorld из SO-5.5.0:

    #include <so_5/all.hpp>
    
    class a_hello_t : public so_5::rt::agent_t
    {
    public:
       a_hello_t( so_5::rt::environment_t & env ) : so_5::rt::agent_t( env )
       {}
    
       void so_evt_start() override
       {
          std::cout << "Hello, world! This is SObjectizer v.5." << std::endl;
          so_environment().stop();
       }
    
       void so_evt_finish() override
       {
          std::cout << "Bye! This was SObjectizer v.5." << std::endl;
       }
    };
    
    int main()
    {
       try
       {
          so_5::launch( []( so_5::rt::environment_t & env ) {
             env.register_agent_as_coop( "coop", new a_hello_t( env ) );
          } );
       }
       catch( const std::exception & ex )
       {
          std::cerr << "Error: " << ex.what() << std::endl;
          return 1;
       }
    
       return 0;
    }

    Само сокращение «rt» расшифровывается как «run-time». И нам казалось, что запись «so_5::rt» гораздо лучше и практичнее, чем «so_5::runtime».

    Но оказалось, что у многих людей «rt» — это только «real-time» и никак иначе. А использование «rt» как сокращение для «runtime» нарушает их чувства настолько сильно, что иногда анонсы версий SObjectizer-а в Рунете превращались в холивар на тему [не]допустимости трактовки «rt» иначе, нежели «real-time».

    В конце-концов нам это надоело. И мы просто задеприкейтили пространство имен «so_5::rt».

    Что стало?


    Все, что было определено внутри «so_5::rt» перехало просто в «so_5». В результате тот же самый HelloWorld сейчас выглядит следующим образом:

    #include <so_5/all.hpp>
    
    class a_hello_t : public so_5::agent_t
    {
       public:
          a_hello_t( context_t ctx ) : so_5::agent_t( ctx )
          {}
    
          void so_evt_start() override
          {
             std::cout << "Hello, world! This is SObjectizer v.5 ("
                   << SO_5_VERSION << ")" << std::endl;
             so_environment().stop();
          }
    
          void so_evt_finish() override
          {
             std::cout << "Bye! This was SObjectizer v.5." << std::endl;
          }
    };
    
    int main()
    {
       try
       {
          so_5::launch( []( so_5::environment_t & env ) {
             env.register_agent_as_coop( "coop", env.make_agent<a_hello_t>() );
          } );
       }
       catch( const std::exception & ex )
       {
          std::cerr << "Error: " << ex.what() << std::endl;
          return 1;
       }
    
       return 0;
    }

    Но старые имена из «so_5::rt» остались доступны все равно, через обычные using-и (typedef-ы). Так что код, написанный для первых версий SO-5.5 оказывается работоспособным и в свежих версиях SO-5.5.

    Окончательно пространство имен «so_5::rt» будет удалено в версии 5.6.

    Какое влияние оказало?


    Наверное, код на SObjectizer-е теперь оказывается более читабельным. Все-таки «so_5::send()» воспринимается лучше, чем «so_5::rt::send()».

    Ну а у нас, как у разработчиков SObjectizer-а, головной боли поубавилось. Вокруг анонсов SObjectizer-а в свое время и так было слишком много пустой болтовни и ненужных рассуждений (начиная от вопросов «Зачем нужны акторы в C++ вообще» и заканчивая «Почему вы не используете PascalCase для именования сущностей»). Одной флеймоопасной темой стало меньше и это было хорошо :)

    Упрощение отсылки сообщений и эволюция обработчиков сообщений


    Что было?


    Еще в самых первых версиях SObjectizer-5.5 отсылка обычного сообщения выполнялась посредством метода deliver_message, который нужно было вызвать у mbox-а получателя. Для отсылки отложенного или периодического сообщения нужно было вызывать single_timer/schedule_timer у объекта типа environment_t. А уже отсылка синхронного запроса другому агенту вообще требовала целой цепочки операций. Вот, например, как это все могло выглядеть четыре года назад (здесь уже используется std::make_unique(), который в C++11 еще не был доступен):

    
    // Отсылка обычного сообщения.
    mbox->deliver_message(std::make_unique<my_message>(...));
    // Отсылка отложенного сообщения.
    env.single_timer(std::make_unique<my_message>(...), mbox, std::chrono::seconds(2));
    // Отсылка периодического сообщения.
    auto timer_id = env.schedule_timer(
       std::make_unique<my_message>(...),
       mbox, std::chrono::seconds(2), std::chrono::seconds(5));
    // Отсылка синхронного запроса с ожидание ответа в течении 10 секунд.
    auto reply = mbox->get_one<std::string>()
       .wait_for(std::chrono::seconds(10))
       .sync_get(std::make_unique<my_message>(...)); 
    

    Кроме того, формат обработчиков сообщений в SObjectizer к версии 5.5 эволюционировал. Если первоначально в SObjectizer-5 все обработчики должны были иметь формат:

    void evt_handler(const so_5::event_data_t<Msg> & cmd);

    то со временем к разрешенным форматам добавились еще несколько:

    
    // Для случая, когда Msg -- это сообщение, а не сигнал.
    ret_value evt_handler(const Msg & msg);
    ret_value evt_handler(Msg msg);
    
    // Для случая, когда обработчик вешается на сигнал.
    ret_value evt_handler();
    

    Новые форматы обработчиков стали широко использоваться, т.к. постоянно расписывать «const so_5::event_data_t<Msg>&» — это то еще удовольствие. Но, с другой стороны, более простые форматы оказались не дружественными агентам-шаблонам. Например:

    template<typename Msg_To_Process>
    class my_actor : public so_5::agent_t {
       void on_receive(const Msg_To_Process & msg) { // Oops!
          ...
       }
    };

    Такой шаблонный агент будет работать только если Msg_To_Process — это тип сообщения, а не сигнала.

    Что стало?


    В ветке 5.5 появилось и существенно эволюционировало семейство send-функций. Для этого пришлось, во-первых, получить в свое распоряжение компиляторы с поддержкой variadic templates. И, во-вторых, накопить достаточный опыт работы, как с variadic templates вообще, так и с первыми версиями send-функций. Причем в разных контекстах: и в обычных агентах, и в ad-hoc-агентах, и в агентах, которые реализуются шаблонными классами, и вне агентов вообще. В том числе и при использовании send-функций с mchain-ами (о них речь пойдет ниже).

    В дополнение к send-функциям появились и функции request_future/request_value, которые предназначены для синхронного взаимодействия между агентами.

    В результате сейчас отсылка сообщений выглядит следующим образом:

    
    // Отсылка обычного сообщения.
    so_5::send<my_message>(mbox, ...);
    // Отсылка отложенного сообщения.
    so_5::send_delayed<my_message>(env, mbox, std::chrono::seconds(2), ...);
    // Отсылка периодического сообщения.
    auto timer_id = so_5::send_periodic<my_message>(
       env, mbox, std::chrono::seconds(2), std::chrono::seconds(5), ...);
    // Отсылка синхронного запроса с ожидание ответа в течении 10 секунд.
    auto reply =so_5::request_value<std::string, my_message>(mbox, std::chrono::seconds(10), ...);

    Добавился еще один возможный формат для обработчиков сообщений. Причем, именно этот формат и будет оставлен в следующих мажорных релизах SObjectizer-а как основной (и, возможно, единственный). Это следующий формат:

    ret_type evt_handler(so_5::mhood_t<Msg> cmd);

    Где Msg может быть как типом сообщения, так и типом сигнала.

    Такой формат не только стирает грань между агентами в виде обычных классов и агентов в виде шаблонных классов. Но еще и упрощает перепосылку сообщения/сигнала (спасибо семейству функций send):

    void my_agent::on_msg(mhood_t<Some_Msg> cmd) {
       ... // Какие-то собственные действия.
       // Делегируем обработку этого же сообщения другому агенту.
       so_5::send(another_agent, std::move(cmd));
    }

    Какое влияние оказало?


    Появление send-функций и обработчиков сообщений, получающих mhood_t<Msg>, можно сказать, принципиально изменило код, в котором сообщения отсылаются и обрабатываются. Это как раз тот случай, когда остается только пожалеть, что в самом начале работ над SObjectizer-5 у нас не было ни компиляторов с поддержкой variadic templates, ни опыта их использования. Семейство send-функций и mhood_t следовало бы иметь с самого начала. Но история сложилась так, как сложилась…

    Поддержка сообщений пользовательских типов


    Что было?


    Первоначально все отсылаемые сообщения должны были быть классами-наследниками класса so_5::message_t. Например:

    struct my_message : public so_5::message_t {
       ... // Атрибуты my_message.
       my_message(...) : ... {...} // Конструктор для my_message.
    };

    Пока пятым SObjectizer-ом пользовались только мы сами, это не вызывало никаких вопросов. Ну вот так и вот так.

    Но как только SObjectizer-ом начали интересоваться сторонние пользователи, мы сразу же столкнулись с регулярно повторяющимся вопросом: «А я обязательно должен наследовать сообщение от so_5::message_t?» Особенно актуальным этот вопрос был в ситуациях, когда нужно было отсылать в качестве сообщений объекты типов, на которые пользователь повлиять вообще не мог. Скажем, пользователь использует SObjectizer и еще какую-то внешнюю библиотеку. И в этой внешней библиотеке есть некий тип M, объекты которых пользователь хотел бы отсылать в качестве сообщений. Ну и как в таких условиях подружить тип M и so_5::message_t? Только дополнительными обертками, которые пользователь должен был писать вручную.

    Что стало?


    Мы добавили в SObjectizer-5.5 возможность отсылать сообщения даже в случае, если тип сообщения не наследуется от so_5::message_t. Т.е. сейчас пользователь может запросто написать:

    so_5::send<std::string>(mbox, "Hello, World!");

    Под капотом все равно остается so_5::message_t, просто за счет шаблонной магии send() понимает, что std::string не наследуется от so_5::message_t и внутри send-а конструируется не простой std::string, а специальный наследник от so_5::message_t, внутри которого уже находится нужный пользователю std::string.

    Похожая шаблонная магия применяется и при подписке. Когда SObjectizer видит обработчик сообщения вида:

    void evt_handler(mhood_t<std::string> cmd) {...}

    то SObjectizer понимает, что на самом деле придет специальное сообщение с объектом std::string внутри. И что нужно вызвать обработчик с передачей в него ссылки на std::string из этого специального сообщения.

    Какое влияние оказало?


    Использовать SObjectizer стало проще, особенно когда в качестве сообщений нужно отсылать не только объекты своих собственных типы, но и объекты типов из внешних библиотек. Несколько человек даже нашли время сказать отдельное спасибо именно за эту фичу.

    Мутабельные сообщения


    Что было?


    Изначально в SObjectizer-5 использовалась только модель взаимодействия 1:N. Т.е. у отосланного сообщения могло быть более одного получателя (а могло быть и не одного). Даже если агентам нужно было взаимодействовать в режиме 1:1, то они все равно общались через multi-producer/multi-consumer почтовый ящик. Т.е. в режиме 1:N, просто N в этом случае было строго единица.

    В условиях, когда сообщение может быть получено более чем одним агентом-получателем, отсылаемые сообщения должны быть иммутабельными. Именно поэтому обработчики сообщений имели следующие форматы:

    
    // Доступ к сообщению только через константную ссылку.
    ret_type evt_handler(const event_data_t<Msg> & cmd);
    // Доступ к сообщению только через константную ссылку.
    ret_type evt_handler(const Msg & msg);
    // Доступ к копии сообщения.
    // Модификация этой копии не сказывается на других копиях.
    ret_type evt_handler(Msg msg);

    В общем-то, простой и понятный подход. Однако, не очень удобный, когда агентам нужно общаться друг с другом в режиме 1:1 и, например, передавать друг-другу владение какими-то данными. Скажем, вот такое простое сообщение не сделать, если все сообщения — это строго иммутабельные объекты:

    struct process_image : public so_5::message_t {
       std::unique_ptr<gif_image> image_;
       process_image(std::unique_ptr<gif_image> image) : image_{std::move(image)) {}
    };

    Точнее говоря, отослать-то такое сообщение можно было бы. Но вот получив его как константный объект, изъять к себе содержимое process_image::image_ уже просто так не получилось бы. Пришлось бы помечать такой атрибут как mutable. Но тогда мы бы теряли контроль со стороны компилятора в случае, когда process_image почему-то отсылается в режиме 1:N.

    Что стало?


    В SObjectizer-5.5 была добавлена возможность отсылать и получать мутабельные сообщения. При этом пользователь должен специальным образом помечать сообщение и при отсылке, и при подписке на него.

    Например:

    // Отсылаем обычное иммутабельное сообщение.
    so_5::send<my_message>(mbox, ...);
    // Отсылаем мутабельное сообщение типа my_message.
    so_5::send<so_5::mutable_msg<my_message>>(mbox, ...);
    ...
    // Обработчик для обычного иммутабельного сообщения.
    void my_agent::on_some_event(mhood_t<my_message> cmd) {...}
    // Обработчик для мутабельного сообщения типа my_message.
    void my_agent::on_another_event(mhood_t<so_5::mutable_msg<my_message>> cmd) {...}
    

    Для SObjectizer-а my_message и mutable_msg<my_message> — это два разных типа сообщений.

    Когда send-функция видит, что ее просят отослать мутабельное сообщение, то send-функция проверяет, а в какой почтовый ящик это сообщение пытаются отослать. Если это multi-consumer ящик, то отсылка не выполняется, а выбрасывается исключение с соответствующим кодом ошибки. Т.е. SObjectizer гарантирует, что мутабельные сообщение могут использоваться только при взаимодействии в режиме 1:1 (через single-consumer ящики или mchain-ы, которые являются разновидностью single-consumer ящиков). Для обеспечения этой гарантии, кстати говоря, SObjectizer запрещает отсылку мутабельных сообщений в виде периодических сообщений.

    Какое влияние оказало?


    С мутабельными сообщениями оказалось неожиданно. Мы их добавили в SObjectizer в результате обсуждения в кулуарах доклада про SObjectizer на C++Russia-2017. С ощущением «ну раз просят, значит кому-то нужно, поэтому стоит попробовать». Ну и сделали без особых надежд на широкую востребованность. Хотя для этого пришлось очень долго «курить бамбук» прежде чем придумалось как мутабельные сообщения добавить в SO-5.5 не поломав совместимость.

    Но вот когда мутабельные сообщения в SObjectizer появились, то оказалось, что применений для них не так уж и мало. И что мутабельные сообщения используются на удивление часто (упоминания об этом можно найти во второй части рассказа про демо-проект Shrimp). Так что на практике эта фича оказалась более чем полезна, т.к. она позволяет решить проблемы, которые без поддержки мутабельных сообщений на уровне SObjectizer-а, нормального решения не имели.

    Агенты в виде иерархических конечных автоматов


    Что было?


    Агенты в SObjectizer изначально были конечными автоматами. У агентов нужно было явным образом описывать состояния и делать подписки на сообщения в конкретных состояниях.
    Например:

    class worker : public so_5::agent_t {
       state_t st_free{this, "free"};
       state_t st_bufy{this, "busy"};
    ...
       void so_define_agent() override {
          // Делаем подписку для состояния st_free.
          so_subscribe(mbox).in(st_free).event(...);
          // Делаем подписку для состояния st_busy.
          so_subscribe(mbox).in(st_busy).event(...);
          ...
       }
    };

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

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

    Что стало?


    В SObjectizer появилась поддержка иерархических конечных автоматов.

    Теперь состояния могут быть вложены друг в друга. Обработчики событий из родительских состояний автоматически «наследуются» дочерними состояниями.

    Поддерживаются обработчики входа в состояние и выхода из него.

    Есть возможность задать ограничение на время пребывания агента в состоянии.

    Есть возможность хранить историю для состояния.

    Дабы не быть голословным, вот пример агента, который является не сложным иерархическим конечным автоматом (код из штатного примера blinking_led):

    class blinking_led final : public so_5::agent_t {
       state_t off{ this }, blinking{ this },
          blink_on{ initial_substate_of{ blinking } },
          blink_off{ substate_of{ blinking } };
    public :
       struct turn_on_off final : public so_5::signal_t {};
    
       blinking_led( context_t ctx ) : so_5::agent_t{ ctx } {
          this >>= off;
    
          off.just_switch_to< turn_on_off >( blinking );
    
          blinking.just_switch_to< turn_on_off >( off );
    
          blink_on
             .on_enter( []{ std::cout << "ON" << std::endl; } )
             .on_exit( []{ std::cout << "off" << std::endl; } )
             .time_limit( std::chrono::milliseconds{1250}, blink_off );
    
          blink_off
             .time_limit( std::chrono::milliseconds{750}, blink_on );
       }
    };

    Все это мы уже описывали в отдельной статье, нет необходимости повторяться.

    В настоящий момент нет поддержки ортогональных состояний. Но у этого факта есть два объяснения. Во-первых, мы пробовали сделать эту поддержку и столкнулись с рядом сложностей, преодоление которых нам показалось слишком дорогостоящим. И, во-вторых, пока еще ортогональные состояния никто у нас не просил. Когда попросят, тогда вновь вернемся к этой теме.

    Какое влияние оказало?


    Есть ощущение, что очень серьезное (хотя мы здесь, конечно же, субъективны и пристрастны). Ведь одно дело, когда сталкиваясь со сложными конечными автоматами в предметной области ты начинаешь искать обходные пути, что-то упрощать, на что-то тратить дополнительные силы. И совсем другое дело, когда ты можешь объекты из своей прикладной задачи отобразить на свой C++ный код чуть ли не 1-в-1.

    Кроме того, судя по вопросам, которые задают, например, по поведению обработчиков входа/выхода в/из состояния, этой функциональностью пользуются.

    mchain-ы


    Что было?


    Была интересная ситуация. SObjectizer нередко использовался так, что только часть приложения была написана на SObjectizer-е. Остальной код в приложении мог не иметь никакого отношения ни к акторам вообще, ни к SObjectizer-у в частности. Например, GUI-приложение, в котором SObjectizer применяется для каких-то фоновых задач, тогда как основная работа выполняется на главном потоке приложения.

    И вот в таких случаях оказалось, что из не-SObjectizer-части внутрь SObjectizer-части отсылать информацию проще простого: достаточно вызывать обычные send-функции. А вот с распространением информации в обратную сторону не все так просто. Нам показалось, что это не есть хорошо и что следует иметь какие-то удобные каналы общения SObjectizer-частей приложения с не-SObjectizer-частями прямо «из коробки».

    Что стало?


    Так в SObjectizer появились message chains или, в более привычной нотации, mchains.

    Mchain — это такой специфический вариант single-consumer почтового ящика, в который сообщения отсылаются обычными send-функциями. Но вот для извлечения сообщений из mchain не нужно создавать агентов и подписывать их. Есть две специальные функции, которые можно вызывать хоть внутри агентов, хоть вне агентов: receive() и select(). Первая читает сообщения только из одного канала, тогда как вторая может читать сообщения сразу из нескольких каналов:

    using namespace so_5;
    mchain_t ch1 = env.create_mchain(...);
    mchain_t ch2 = env.create_mchain(...);
    
    select( from_all().handle_n(3).empty_timeout(200ms),
      case_(ch1,
          [](mhood_t<first_message_type> msg) { ... },
          [](mhood_t<second_message_type> msg) { ... }),
      case_(ch2,
          [](mhood_t<third_message_type> msg ) { ... },
          [](mhood_t<some_signal_type>){...},
          ... ));

    Про mchain-ы мы уже несколько раз здесь рассказывали: в августе 2017-го и в мае 2018. Поэтому особо на тему того, как выглядит работа с mchain-ами углубляться здесь не будем.

    Какое влияние оказало?


    После появления mchain-ов в SObjectizer-5.5 оказалось, что SObjectizer, по факту, стал еще менее «акторным» фреймворком, чем он был до этого. К поддержке Actor Model и Pub/Sub, в SObjectizer-е добавилась еще и поддержка модели CSP (communicating sequential processes). Mchain-ы позволяют разрабатывать достаточно сложные многопоточные приложения на SObjectizer вообще без акторов. И для каких-то задач это оказывается более чем удобно. Чем мы сами и пользуемся время от времени.

    Механизм message limits


    Что было?


    Одним из самых серьезных недостатков Модели Акторов является предрасположенность к возникновению перегрузок. Очень легко оказаться в ситуации, когда актор-отправитель отсылает сообщения актору-получателю с более высоким темпом, чем актор-получатель может обрабатывать сообщения.

    Как правило, отсылка сообщений в акторных фреймворках — это неблокирующая операция. Поэтому при возникновении пары «шустрый-producer и тормозной-consumer» очередь у актора-получателя будет увеличиваться пока остается хоть какая-то свободная память.

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

    Еще одна сложность в том, что не всегда нужен именно хороший механизм защиты. Временами достаточно иметь что-то примитивное, но действенное, доступное «из коробки» и простое в использовании. Чтобы не заставлять пользователя делать свой overload control там, где достаточно просто выбрасывать «лишние» сообщения или пересылать эти сообщения какому-то другому агенту.

    Что стало?


    Как раз для того, чтобы в простых сценариях можно было воспользоваться готовыми средствами защиты от перегрузки, в SObjectizer-5.5 были добавлены т.н. message limits. Этот механизм позволяет отбрасывать лишние сообщения, или пересылать их другим получателям, либо вообще просто прерывать работу приложения, если лимиты превышены. Например:

    class worker : public so_5::agent_t {
    public:
       worker(context_t ctx) : so_5::agent_t{ ctx
          // Если в очереди больше 100 сообщений handle_data,
          // то последующие сообщения должны пересылаться
          // другому агенту.
          + limit_then_redirect<handle_data>(100, [this]{ return another_worker_;})
          // Если в очереди больше 1 сообщения check_status,
          // то остальные можно выбросить и не обрабатывать.
          + limit_then_drop<check_status>(1)
          // Если в очереди больше 1 сообщения reconfigure,
          // то работу приложения нужно прерывать, т.к. последующий reconfigure
          // не может быть отослан пока не обработан предыдущий.
          + limit_then_abore<reconfigure>(1) }
       {...}
       ...
    };

    Более подробно эта тема раскрывается в отдельной статье.

    Какое влияние оказало?


    Нельзя сказать, что появление message limits стало чем-то, что кардинально изменило SObjectizer, принципы его работы или работу с ним. Скорее это можно сравнить с запасным парашютом, который используется лишь в крайнем случае. Но когда приходится его использовать, то оказываешься рад, что он вообще есть.

    Механизм message delivery tracing


    Что было?


    SObjectizer-5 был для разработчиков «черным ящиком». В который сообщение отсылается и… И оно либо приходит к получателю, либо не приходит.

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

    Проблема была в том, что механизм доставки сообщений упрятан глубоко в потрохах SObjectizer Run-Time и, поэтому, протрассировать процесс доставки сообщения до получателя было сложно даже разработчикам SObjectizer-а, не говоря уже про пользователей. Особенно про начинающих пользователей, которые и совершали наибольшее количество таких тривиальных ошибок.

    Что стало?


    В SObjectizer-5.5 был добавлен, а затем и доработан, специальный механизм трассировки процесса доставки сообщений под названием message delivery tracing (или просто msg_tracing). Подробнее этот механизм и его возможности описывался в отдельной статье.

    Так что теперь, если сообщения теряются при доставке, можно просто включить msg_tracing и посмотреть, почему это происходит.

    Какое влияние оказало?


    Отладка написанных на SObjectizer приложений стала гораздо более простым и приятным делом. Даже для нас самих.

    Понятие env_infrastructure и однопоточные env_infrastructures


    Что было?


    Нами SObjectizer всегда рассматривался как инструмент для упрощения разработки многопоточного кода. Поэтому первые версии SObjectizer-5 были написаны так, чтобы работать только в многопоточной среде.

    Это выражалось как в использовании примитивов синхронизации внутри SObjectizer-а для защиты внутренностей SObjectizer-а при работе в многопоточной среде. Так и в создании нескольких вспомогательных рабочих нитей внутри самого SObjectizer-а (для выполнения таких важных операций, как обслуживание таймера и завершение дерегистрации коопераций агентов).

    Т.е. SObjectizer был создан для многопоточного программирования и для использования в многопоточной среде. И нас это вполне устраивало.

    Однако, по мере использования SObjectizer-а «в дикой природе» обнаруживались ситуации, когда задача была достаточно сложной для того, чтобы в ее решении использовались акторы. Но, при этом, всю работу можно и, более того, нужно было выполнять на одном единственном рабочем потоке.

    И мы встали перед весьма интересной проблемой: а можно ли научить SObjectizer работать на одной-единственной рабочей нити?

    Что стало?


    Оказалось, что можно.

    Обошлось нам это недешево, было потрачено много времени и сил на то, чтобы придумать решение. Но решение было придумано.

    Было введено такое понятие, как environment infrastructure (или env_infrastructure в немного сокращенном виде). Env_infrastructure брал на себя задачи управления внутренней кухней SObjectizer-а. В частности, решал такие вопросы, как обслуживание таймеров, выполнение операций регистрации и дерегистрации коопераций.

    Для SObjectizer-а было сделано несколько вариантов однопоточных env_infrastructures. Что позволило разрабатывать на SObjectizer однопоточные приложения, внутри которых существуют нормальные агенты, обменивающиеся друг с другом обычными сообщениями.

    Подробнее об этой функциональности мы рассказывали в отдельной статье.

    Какое влияние оказало?


    Пожалуй, самое важное, что произошло при внедрении данной фичи — это разрыв наших собственных шаблонов. Взгляд на SObjectizer уже никогда не будет прежним. Столько лет рассматривать SObjectizer исключительно как инструмент для разработки многопоточного кода. А потом раз! И обнаружить, что однопоточный код на SObjectizer-е также может разрабатываться. Жизнь полна неожиданностей.

    Средства run-time мониторинга


    Что было?


    SObjectizer-5 был черным ящиком не только в отношении механизма доставки сообщений. Но так же не было средств узнать, сколько агентов сейчас работает внутри приложения, сколько и каких диспетчеров создано, сколько у них рабочих нитей задействовано, сколько сообщений ждут в очередях диспетчеров и т.д.

    Вся эта информация весьма полезна для контроля за приложениями, работающими в режиме 24/7. Но и для отладки так же временами хотелось бы понимать, растут ли очереди или увеличивается/уменьшается ли количество агентов.

    К сожалению, до поры, до времени у нас просто руки не доходили до того, чтобы добавить в SObjectizer средства для сбора и распространения подобной информации.

    Что стало?


    В один прекрасный момент в SObjectizer-5.5 появились средства для run-time мониторинга внутренностей SObjectizer-а. По умолчанию run-time мониторинг отключен, но если его включить, то в специальный mbox регулярно будут отсылаться сообщения, внутри которых будет информация о количестве агентов и коопераций, о количестве таймеров, о рабочих нитях, которыми владеют диспетчеры (а там уже будет информация о количестве сообщений в очередях, количестве агентов, привязанных к этим нитям).

    Плюс со временем появилась возможность дополнительно включить сбор информации о том, сколько времени агенты проводят внутри обработчиков событий. Что позволяет обнаруживать ситуации, когда какой-то агент работает слишком медленно (либо тратит время на блокирующих вызовах).

    Какое влияние оказало?


    В нашей практике run-time мониторинг используется не часто. Но, когда он нужен, то тогда осознаешь его важность. Ведь без такого механизма бывает невозможно (ну или очень сложно) разобраться с тем, что и как [не]работает.

    Так что это фича из категории «можно и обойтись», но ее наличие, на наш взгляд, сразу же переводит инструмент в другую весовую категорию. Т.к. сделать прототип акторного фреймворка «на коленке» не так уж и сложно. Многие делали это и еще многие будут это делать. Но вот снабдить затем свою разработку такой штукой, как run-time мониторинг… До этого доживают далеко не все наколенные наброски.

    И еще кое-что одной строкой


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

    В SObjectizer-5.5 добавлена поддержка системы сборки CMake.

    Теперь SObjectizer-5 можно собирать и как динамическую, и как статическую библиотеку.

    SObjectizer-5.5 теперь собирается и работает под Android (как посредством CrystaX NDK, так и посредством свежих Android NDK).

    Появились приватные диспетчеры. Теперь можно создавать и использовать диспетчеры, которые никто кроме вас не видит.

    Реализован механизм delivery filters. Теперь при подписке на сообщения из MPMC-mbox-ов можно запретить доставку сообщений, чье содержимое вам не интересно.

    Существенно упрощены средства создания и регистрации коопераций: методы introduce_coop/introduce_child_coop, make_agent/make_agent_with_binder и вот это вот все.

    Появилось понятие фабрики lock-объектов и теперь можно выбирать какие lock-объекты вам нужны (на базе mutex-ов, spinlock-ов, комбинированных или каких-то еще).

    Появился класс wrapped_env_t и теперь запускать SObjectizer в своем приложении можно не только посредством so_5::launch().

    Появилось понятие stop_guard-ов и теперь можно влиять на процесс завершения работы SObjectizer-а. Например, можно запретить SObjectizer-у останавливаться пока какие-то агенты не завершили свою прикладную работу.

    Появилась возможность перехватывать сообщения, которые были доставлены до агента, но не были агентом обработаны (т.н. dead_letter_handlers).

    Появилась возможность оборачивать сообщения в специальные «конверты». Конверты могут нести дополнительную информацию о сообщении и могут выполнять какие-то действия когда сообщение доставлено до получателя.

    От 5.5.0 до 5.5.23 в цифрах


    Любопытно также взглянуть на проделанный путь с точки зрения объема кода/тестов/примеров. Вот что нам говорит утилита cloc про объем кода ядра SObjectizer-5.5.0:

    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C/C++ Header                    58           2119           5156           5762
    C++                             39           1167            779           4759
    Ruby                             2             30              2             75
    -------------------------------------------------------------------------------
    SUM:                            99           3316           5937          10596
    -------------------------------------------------------------------------------
    

    А вот тоже самое, но уже для v.5.5.23 (из них 1147 строк — это код библиотеки optional-lite):
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C/C++ Header                   133           6279          22173          21068
    C++                             53           2498           2760          10398
    CMake                            2             29              0            177
    Ruby                             4             53              2            129
    -------------------------------------------------------------------------------
    SUM:                           192           8859          24935          31772
    -------------------------------------------------------------------------------
    

    Объем тестов для v.5.5.0:
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C++                             84           2510            390          11540
    Ruby                           162            496              0           1054
    C/C++ Header                     1             11              0             32
    -------------------------------------------------------------------------------
    SUM:                           247           3017            390          12626
    -------------------------------------------------------------------------------
    

    Тесты для v.5.5.23:
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C++                            324           7345           1305          35231
    Ruby                           675           2353              0           4671
    CMake                          338             43              0            955
    C/C++ Header                    11            107              3            448
    -------------------------------------------------------------------------------
    SUM:                          1348           9848           1308          41305
    

    Ну и примеры для v.5.5.0:
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C++                             27            765            463           3322
    Ruby                            28             95              0            192
    -------------------------------------------------------------------------------
    SUM:                            55            860            463           3514
    

    Они же, но уже для v.5.5.23:
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C++                             67           2141           2061           9341
    Ruby                           133            451              0            868
    CMake                           67             93              0            595
    C/C++ Header                     1             12             11             32
    -------------------------------------------------------------------------------
    SUM:                           268           2697           2072          10836
    

    Практически везде увеличение почти в три раза.

    А объем документации для SObjectizer-а, наверное, увеличился даже еще больше.

    Планы на ближайшее (и не только) будущее


    Предварительные планы развития SObjectizer-а после релиза версии 5.5.23 были описаны здесь около месяца назад. Принципиально они не поменялись. Но появилось ощущение, что версию 5.6.0, релиз которой запланирован на начало 2019-го года, нужно будет позиционировать как начало очередной стабильной ветки SObjectizer-а. С прицелом на то, что в течении 2019-го года SObjectizer будет развиваться в рамках ветки 5.6 без каких-либо существенных ломающих изменений.

    Это даст возможность тем, кто сейчас использует SO-5.5 в своих проектах, постепенно перейти на SO-5.6 без опасения о том, что следом придется еще и переходить на SO-5.7.

    Версия же 5.7, в которой мы хотим себе позволить отойти где-то от базовых принципов SO-5.5 и SO-5.6, в 2019-ом году будет рассматриваться как экспериментальная. Со стабилизацией и релизом, если все будет хорошо, уже в 2020-ом году.

    Заключение


    В заключении хотелось бы поблагодарить всех, кто помогал нам с развитием SObjectizer-а все это время. И отдельно хочется сказать спасибо всем тем, кто рискнул попробовать поработать с SObjectizer-ом. Ваш фидбэк всегда был нам очень полезен.

    Тем же, кто еще не пользовался SObjectizer-ом хотим сказать: попробуйте. Это не так страшно, как может показаться.

    Если вам что-то не понравилось или не хватило в SObjectizer — скажите нам. Мы всегда прислушиваемся к конструктивной критике. И, если это в наших силах, воплощаем пожелания пользователей в жизнь.
    Поделиться публикацией
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое