Pull to refresh

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

Reading time22 min
Views1.3K
Первая версия 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 — скажите нам. Мы всегда прислушиваемся к конструктивной критике. И, если это в наших силах, воплощаем пожелания пользователей в жизнь.
Tags:
Hubs:
Total votes 14: ↑12 and ↓2+10
Comments0

Articles