Первая версия SObjectizer-а в рамках ветки 5.5 вышла чуть больше четырех лет назад — в начале октября 2014-го года. А сегодня увидела свет очередная версия под номером 5.5.23, которая, вполне возможно, закроет историю развития SObjectizer-5.5. По-моему, это отличный повод оглянуться назад и посмотреть, что же было сделано за минувшие четыре года.
В этой статье я попробую тезисно разобрать наиболее важные и знаковые изменения и нововведения: что было добавлено, зачем, как это повлияло на сам SObjectizer или его использование.
Возможно, кому-то такой рассказ будет интересен с точки зрения археологии. А кого-то, возможно, удержит от такого сомнительного приключения, как разработка собственного акторного фреймворка для C++ ;)
История SObjectizer-5 началась в середине 2010-го года. При этом мы сразу ориентировались на C++0x. Уже в 2011-ом первые версии SObjectizer-5 стали использоваться для написания production-кода. Понятное дело, что компиляторов с нормальной поддержкой C++11 у нас тогда не было.
Долгое время мы не могли использовать в полной мере все возможности «современного C++»: variadic templates, noexcept, constexpr и пр. Это не могло не сказаться на API SObjectizer-а. И сказывалось еще очень и очень долго. Поэтому, если при чтении описания какой-то фичи у вас возникает вопрос «А почему так не было сделано раньше?», то ответ на такой вопрос скорее всего: «Потому, что раньше не было возможности».
В данном разделе мы пройдемся по ряду фич, которые оказали существенное влияние на SObjectizer. Порядок следования в этом списке случаен и не имеет отношения к «значимости» или «весу» описываемых фич.
Изначально в пятом 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:
Само сокращение «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 сейчас выглядит следующим образом:
Но старые имена из «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 еще не был доступен):
Кроме того, формат обработчиков сообщений в SObjectizer к версии 5.5 эволюционировал. Если первоначально в SObjectizer-5 все обработчики должны были иметь формат:
то со временем к разрешенным форматам добавились еще несколько:
Новые форматы обработчиков стали широко использоваться, т.к. постоянно расписывать «const so_5::event_data_t<Msg>&» — это то еще удовольствие. Но, с другой стороны, более простые форматы оказались не дружественными агентам-шаблонам. Например:
Такой шаблонный агент будет работать только если Msg_To_Process — это тип сообщения, а не сигнала.
В ветке 5.5 появилось и существенно эволюционировало семейство send-функций. Для этого пришлось, во-первых, получить в свое распоряжение компиляторы с поддержкой variadic templates. И, во-вторых, накопить достаточный опыт работы, как с variadic templates вообще, так и с первыми версиями send-функций. Причем в разных контекстах: и в обычных агентах, и в ad-hoc-агентах, и в агентах, которые реализуются шаблонными классами, и вне агентов вообще. В том числе и при использовании send-функций с mchain-ами (о них речь пойдет ниже).
В дополнение к send-функциям появились и функции request_future/request_value, которые предназначены для синхронного взаимодействия между агентами.
В результате сейчас отсылка сообщений выглядит следующим образом:
Добавился еще один возможный формат для обработчиков сообщений. Причем, именно этот формат и будет оставлен в следующих мажорных релизах SObjectizer-а как основной (и, возможно, единственный). Это следующий формат:
Где Msg может быть как типом сообщения, так и типом сигнала.
Такой формат не только стирает грань между агентами в виде обычных классов и агентов в виде шаблонных классов. Но еще и упрощает перепосылку сообщения/сигнала (спасибо семейству функций send):
Появление send-функций и обработчиков сообщений, получающих mhood_t<Msg>, можно сказать, принципиально изменило код, в котором сообщения отсылаются и обрабатываются. Это как раз тот случай, когда остается только пожалеть, что в самом начале работ над SObjectizer-5 у нас не было ни компиляторов с поддержкой variadic templates, ни опыта их использования. Семейство send-функций и mhood_t следовало бы иметь с самого начала. Но история сложилась так, как сложилась…
Первоначально все отсылаемые сообщения должны были быть классами-наследниками класса so_5::message_t. Например:
Пока пятым SObjectizer-ом пользовались только мы сами, это не вызывало никаких вопросов. Ну вот так и вот так.
Но как только SObjectizer-ом начали интересоваться сторонние пользователи, мы сразу же столкнулись с регулярно повторяющимся вопросом: «А я обязательно должен наследовать сообщение от so_5::message_t?» Особенно актуальным этот вопрос был в ситуациях, когда нужно было отсылать в качестве сообщений объекты типов, на которые пользователь повлиять вообще не мог. Скажем, пользователь использует SObjectizer и еще какую-то внешнюю библиотеку. И в этой внешней библиотеке есть некий тип M, объекты которых пользователь хотел бы отсылать в качестве сообщений. Ну и как в таких условиях подружить тип M и so_5::message_t? Только дополнительными обертками, которые пользователь должен был писать вручную.
Мы добавили в SObjectizer-5.5 возможность отсылать сообщения даже в случае, если тип сообщения не наследуется от so_5::message_t. Т.е. сейчас пользователь может запросто написать:
Под капотом все равно остается so_5::message_t, просто за счет шаблонной магии send() понимает, что std::string не наследуется от so_5::message_t и внутри send-а конструируется не простой std::string, а специальный наследник от so_5::message_t, внутри которого уже находится нужный пользователю std::string.
Похожая шаблонная магия применяется и при подписке. Когда SObjectizer видит обработчик сообщения вида:
то SObjectizer понимает, что на самом деле придет специальное сообщение с объектом std::string внутри. И что нужно вызвать обработчик с передачей в него ссылки на std::string из этого специального сообщения.
Использовать SObjectizer стало проще, особенно когда в качестве сообщений нужно отсылать не только объекты своих собственных типы, но и объекты типов из внешних библиотек. Несколько человек даже нашли время сказать отдельное спасибо именно за эту фичу.
Изначально в SObjectizer-5 использовалась только модель взаимодействия 1:N. Т.е. у отосланного сообщения могло быть более одного получателя (а могло быть и не одного). Даже если агентам нужно было взаимодействовать в режиме 1:1, то они все равно общались через multi-producer/multi-consumer почтовый ящик. Т.е. в режиме 1:N, просто N в этом случае было строго единица.
В условиях, когда сообщение может быть получено более чем одним агентом-получателем, отсылаемые сообщения должны быть иммутабельными. Именно поэтому обработчики сообщений имели следующие форматы:
В общем-то, простой и понятный подход. Однако, не очень удобный, когда агентам нужно общаться друг с другом в режиме 1:1 и, например, передавать друг-другу владение какими-то данными. Скажем, вот такое простое сообщение не сделать, если все сообщения — это строго иммутабельные объекты:
Точнее говоря, отослать-то такое сообщение можно было бы. Но вот получив его как константный объект, изъять к себе содержимое process_image::image_ уже просто так не получилось бы. Пришлось бы помечать такой атрибут как mutable. Но тогда мы бы теряли контроль со стороны компилятора в случае, когда process_image почему-то отсылается в режиме 1:N.
В SObjectizer-5.5 была добавлена возможность отсылать и получать мутабельные сообщения. При этом пользователь должен специальным образом помечать сообщение и при отсылке, и при подписке на него.
Например:
Для 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 изначально были конечными автоматами. У агентов нужно было явным образом описывать состояния и делать подписки на сообщения в конкретных состояниях.
Например:
Но это были простые конечные автоматы. Состояния не могли быть вложены друг в друга. Не было поддержки обработчиков входа в состояния и выхода из него. Не было ограничений на время пребывания в состоянии.
Даже такая ограниченная поддержка конечных автоматов была удобной и мы пользовались ей не один год. Но в один прекрасный момент нам захотелось большего.
В SObjectizer появилась поддержка иерархических конечных автоматов.
Теперь состояния могут быть вложены друг в друга. Обработчики событий из родительских состояний автоматически «наследуются» дочерними состояниями.
Поддерживаются обработчики входа в состояние и выхода из него.
Есть возможность задать ограничение на время пребывания агента в состоянии.
Есть возможность хранить историю для состояния.
Дабы не быть голословным, вот пример агента, который является не сложным иерархическим конечным автоматом (код из штатного примера blinking_led):
Все это мы уже описывали в отдельной статье, нет необходимости повторяться.
В настоящий момент нет поддержки ортогональных состояний. Но у этого факта есть два объяснения. Во-первых, мы пробовали сделать эту поддержку и столкнулись с рядом сложностей, преодоление которых нам показалось слишком дорогостоящим. И, во-вторых, пока еще ортогональные состояния никто у нас не просил. Когда попросят, тогда вновь вернемся к этой теме.
Есть ощущение, что очень серьезное (хотя мы здесь, конечно же, субъективны и пристрастны). Ведь одно дело, когда сталкиваясь со сложными конечными автоматами в предметной области ты начинаешь искать обходные пути, что-то упрощать, на что-то тратить дополнительные силы. И совсем другое дело, когда ты можешь объекты из своей прикладной задачи отобразить на свой C++ный код чуть ли не 1-в-1.
Кроме того, судя по вопросам, которые задают, например, по поведению обработчиков входа/выхода в/из состояния, этой функциональностью пользуются.
Была интересная ситуация. SObjectizer нередко использовался так, что только часть приложения была написана на SObjectizer-е. Остальной код в приложении мог не иметь никакого отношения ни к акторам вообще, ни к SObjectizer-у в частности. Например, GUI-приложение, в котором SObjectizer применяется для каких-то фоновых задач, тогда как основная работа выполняется на главном потоке приложения.
И вот в таких случаях оказалось, что из не-SObjectizer-части внутрь SObjectizer-части отсылать информацию проще простого: достаточно вызывать обычные send-функции. А вот с распространением информации в обратную сторону не все так просто. Нам показалось, что это не есть хорошо и что следует иметь какие-то удобные каналы общения SObjectizer-частей приложения с не-SObjectizer-частями прямо «из коробки».
Так в SObjectizer появились message chains или, в более привычной нотации, mchains.
Mchain — это такой специфический вариант single-consumer почтового ящика, в который сообщения отсылаются обычными send-функциями. Но вот для извлечения сообщений из mchain не нужно создавать агентов и подписывать их. Есть две специальные функции, которые можно вызывать хоть внутри агентов, хоть вне агентов: receive() и select(). Первая читает сообщения только из одного канала, тогда как вторая может читать сообщения сразу из нескольких каналов:
Про mchain-ы мы уже несколько раз здесь рассказывали: в августе 2017-го и в мае 2018. Поэтому особо на тему того, как выглядит работа с mchain-ами углубляться здесь не будем.
После появления mchain-ов в SObjectizer-5.5 оказалось, что SObjectizer, по факту, стал еще менее «акторным» фреймворком, чем он был до этого. К поддержке Actor Model и Pub/Sub, в SObjectizer-е добавилась еще и поддержка модели CSP (communicating sequential processes). Mchain-ы позволяют разрабатывать достаточно сложные многопоточные приложения на SObjectizer вообще без акторов. И для каких-то задач это оказывается более чем удобно. Чем мы сами и пользуемся время от времени.
Одним из самых серьезных недостатков Модели Акторов является предрасположенность к возникновению перегрузок. Очень легко оказаться в ситуации, когда актор-отправитель отсылает сообщения актору-получателю с более высоким темпом, чем актор-получатель может обрабатывать сообщения.
Как правило, отсылка сообщений в акторных фреймворках — это неблокирующая операция. Поэтому при возникновении пары «шустрый-producer и тормозной-consumer» очередь у актора-получателя будет увеличиваться пока остается хоть какая-то свободная память.
Главная сложность этой проблемы в том, что хороший механизм защиты от перегрузки должен быть заточен под прикладную задачу и особенности предметной области. Например, чтобы понимать, какие сообщения могут дублироваться (и, следовательно, иметь возможность безопасно отбрасывать дубликаты). Чтобы понимать, какие сообщения нельзя выбрасывать в любом случае. Кого можно приостанавливать и на сколько, а кого вообще нельзя. И т.д., и т.п.
Еще одна сложность в том, что не всегда нужен именно хороший механизм защиты. Временами достаточно иметь что-то примитивное, но действенное, доступное «из коробки» и простое в использовании. Чтобы не заставлять пользователя делать свой overload control там, где достаточно просто выбрасывать «лишние» сообщения или пересылать эти сообщения какому-то другому агенту.
Как раз для того, чтобы в простых сценариях можно было воспользоваться готовыми средствами защиты от перегрузки, в SObjectizer-5.5 были добавлены т.н. message limits. Этот механизм позволяет отбрасывать лишние сообщения, или пересылать их другим получателям, либо вообще просто прерывать работу приложения, если лимиты превышены. Например:
Более подробно эта тема раскрывается в отдельной статье.
Нельзя сказать, что появление message limits стало чем-то, что кардинально изменило SObjectizer, принципы его работы или работу с ним. Скорее это можно сравнить с запасным парашютом, который используется лишь в крайнем случае. Но когда приходится его использовать, то оказываешься рад, что он вообще есть.
SObjectizer-5 был для разработчиков «черным ящиком». В который сообщение отсылается и… И оно либо приходит к получателю, либо не приходит.
Если сообщение до получателя не доходит, то пользователь оказывался перед необходимостью пройти увлекательный квест в поисках причины. В большинстве случаев причины тривиальны: либо сообщение отослано не в тот mbox, либо не была сделана подписка (например, пользователь сделал подписку в одном состоянии агента, но забыл сделать ее в другом). Но могут быть и более сложные случаи, когда сообщение, скажем, отвергается механизмом защиты от перегрузки.
Проблема была в том, что механизм доставки сообщений упрятан глубоко в потрохах SObjectizer Run-Time и, поэтому, протрассировать процесс доставки сообщения до получателя было сложно даже разработчикам SObjectizer-а, не говоря уже про пользователей. Особенно про начинающих пользователей, которые и совершали наибольшее количество таких тривиальных ошибок.
В SObjectizer-5.5 был добавлен, а затем и доработан, специальный механизм трассировки процесса доставки сообщений под названием message delivery tracing (или просто msg_tracing). Подробнее этот механизм и его возможности описывался в отдельной статье.
Так что теперь, если сообщения теряются при доставке, можно просто включить msg_tracing и посмотреть, почему это происходит.
Отладка написанных на SObjectizer приложений стала гораздо более простым и приятным делом. Даже для нас самих.
Нами SObjectizer всегда рассматривался как инструмент для упрощения разработки многопоточного кода. Поэтому первые версии SObjectizer-5 были написаны так, чтобы работать только в многопоточной среде.
Это выражалось как в использовании примитивов синхронизации внутри SObjectizer-а для защиты внутренностей SObjectizer-а при работе в многопоточной среде. Так и в создании нескольких вспомогательных рабочих нитей внутри самого SObjectizer-а (для выполнения таких важных операций, как обслуживание таймера и завершение дерегистрации коопераций агентов).
Т.е. SObjectizer был создан для многопоточного программирования и для использования в многопоточной среде. И нас это вполне устраивало.
Однако, по мере использования SObjectizer-а «в дикой природе» обнаруживались ситуации, когда задача была достаточно сложной для того, чтобы в ее решении использовались акторы. Но, при этом, всю работу можно и, более того, нужно было выполнять на одном единственном рабочем потоке.
И мы встали перед весьма интересной проблемой: а можно ли научить SObjectizer работать на одной-единственной рабочей нити?
Оказалось, что можно.
Обошлось нам это недешево, было потрачено много времени и сил на то, чтобы придумать решение. Но решение было придумано.
Было введено такое понятие, как environment infrastructure (или env_infrastructure в немного сокращенном виде). Env_infrastructure брал на себя задачи управления внутренней кухней SObjectizer-а. В частности, решал такие вопросы, как обслуживание таймеров, выполнение операций регистрации и дерегистрации коопераций.
Для SObjectizer-а было сделано несколько вариантов однопоточных env_infrastructures. Что позволило разрабатывать на SObjectizer однопоточные приложения, внутри которых существуют нормальные агенты, обменивающиеся друг с другом обычными сообщениями.
Подробнее об этой функциональности мы рассказывали в отдельной статье.
Пожалуй, самое важное, что произошло при внедрении данной фичи — это разрыв наших собственных шаблонов. Взгляд на SObjectizer уже никогда не будет прежним. Столько лет рассматривать SObjectizer исключительно как инструмент для разработки многопоточного кода. А потом раз! И обнаружить, что однопоточный код на SObjectizer-е также может разрабатываться. Жизнь полна неожиданностей.
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).
Появилась возможность оборачивать сообщения в специальные «конверты». Конверты могут нести дополнительную информацию о сообщении и могут выполнять какие-то действия когда сообщение доставлено до получателя.
Любопытно также взглянуть на проделанный путь с точки зрения объема кода/тестов/примеров. Вот что нам говорит утилита cloc про объем кода ядра SObjectizer-5.5.0:
А вот тоже самое, но уже для v.5.5.23 (из них 1147 строк — это код библиотеки optional-lite):
Объем тестов для v.5.5.0:
Тесты для v.5.5.23:
Ну и примеры для v.5.5.0:
Они же, но уже для v.5.5.23:
Практически везде увеличение почти в три раза.
А объем документации для 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 — скажите нам. Мы всегда прислушиваемся к конструктивной критике. И, если это в наших силах, воплощаем пожелания пользователей в жизнь.
В этой статье я попробую тезисно разобрать наиболее важные и знаковые изменения и нововведения: что было добавлено, зачем, как это повлияло на сам 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 — скажите нам. Мы всегда прислушиваемся к конструктивной критике. И, если это в наших силах, воплощаем пожелания пользователей в жизнь.