LINUX.ORG.RU

SObjectizer v.5.5.22

 , , , ,


1

2

SObjectizer — это относительно небольшой фреймворк для упрощения разработки сложных многопоточных приложений на C++. SObjectizer позволяет разработчику строить свои программы на базе асинхронного обмена сообщениями с использованием таких моделей, как Actor Model, Publish-Subscribe и CSP (в частности, каналов). Это OpenSource проект, распространяется под BSD-3-CLAUSE лицензией.

Мы выпустили очередную версию: SObjectizer-5.5.22.

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

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

Еще в 5.5.22 добавлена возможность использовать свободные функции в качестве обработчиков сообщений в функциях для работы с CSP-шными каналами so_5::receive и so_5::select. И изменено поведение agent_t::so_current_state() — теперь если этот метод вызывается внутри on_enter/on_exit обработчиков, то so_current_state() возвращает ссылку на то состояние, обработчик on_enter/on_exit которого сейчас активен.

SObjectizer живет на SourceForge, есть зеркало на github. Соответственно, исходники могут быть загружены с SF.net или с github-а.

Так же мы обновили надстройку над SObjectizer-ом, библиотеку so_5_extra. so_5_extra обновилась до версии 1.1.0. Но нового в нее ничего не добавилось, только произошел переход на Asio-1.12 и SO-5.5.22. Для этого пришлось перелопатить часть библиотеки и выпустить версию 1.1.0 вместо 1.0.5. Кстати говоря, если вы использовали so_5_extra-1.0.4, то для перехода на SO-5.5.22 вам придется перейти и на so_5_extra-1.1.0, т.к. в SO-5.5.22 сломалась совместимость в той части, где идет работа с кастомными mbox-ами.

so_5_extra живет на SourceForge, взять ее можно оттуда же. Правда, распространяется so_5_extra уже под двойной лицензией.

=====

Отдельно хотелось бы отметить вот какой момент. Мы свой список хотелок для SObjectizer-а исчерпали где-то в районе версии 5.5.19 (т.е. чуть меньше года назад). С тех пор в SO-5.5 добавляются только те фичи, которые кому-нибудь понадобились. Либо нам самим, либо кому-то из пользователей.

С релизом версии 5.5.22 на этом стоит заострить особое внимание: в ветку SObjectizer-5.5 новые фичи теперь попадают только если a) они кому-то нужны и b) нас об этом просят.

Т.е. если вы хотите что-то увидеть в SObjectizer, но нам вы об этом не рассказали и мы об этом не узнали, то в ветке 5.5 вы этого точно не увидите.

=====

Было бы здорово услышать мнение тех, кто смотрел в сторону SObjectizer-а, но не выбрал его в качестве инструмента. Что остановило? Что не понравилось? Что вы не увидели в SObjectizer? Или, напротив, что такого страшного увидели?

Конструктивная обратная связь такого рода поможет нам сделать SObjectizer лучше.

★★★★★

лично мое мнение, учитывая, что либа на c++, то аудитории у нее не много, ибо зачем модель акторов в плюсах особо? ну только если поугарать... мне кажется, как эксперимент, либа прикольная...

xperious ()
Ответ на: комментарий от eao197

да в общем-то нет, ничего сложного не было, только тупой бэкендовский if/else другое дело, что я просто не сильно шарю для чего эта модель акторов нужна, много раз слышал, но так, толком, и не понял

xperious ()
Ответ на: комментарий от xperious

я просто не сильно шарю для чего эта модель акторов нужна

Это просто один из способов борьбы со сложностью. Где-то подходит, где-то не подходит. Чуть подробнее здесь.

Но если модель акторов подходит (или подходит pub/sub, или подходят каналы из CSP), то вместо того, чтобы клепать на коленке что-то свое, можно взять готовое. Благо есть выбор.

eao197 ★★★★★ ()
Ответ на: комментарий от RazrFalcon

Простите, я забыл, что говорю с идиотом. SourceForge предоставляет мощный CDN для OpenSource проектов, чем и пользуются Boost, MinGW-w64, Cream for Vim и еще множество других проектов, включая и нас.

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

eao197 ★★★★★ ()

лучше бы что нибудь практичное написали
что такое практичное ? ну к примеру берем выборку по апворку за пол года по С++ и смотрит что является актуальным, задачи
вот фреймфорки для таких задач и будут практичным

anonymous ()
Ответ на: комментарий от xperious

лично мое мнение, учитывая, что либа на c++, то аудитории у нее не много, ибо зачем модель акторов в плюсах особо? ну только если поугарать... мне кажется, как эксперимент, либа прикольная...

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

anonymous ()
Ответ на: комментарий от anonymous

без плюсового и баззвордового упоринства как у автора в SObjectizer

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

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

А вот и очередной всезнающий эксперт из породы «никто и звать никак».

Что это тут такое ... прёт на ружу комплекс неполноценности быдлядской душонки считающей своё жалкое поделие вершиной технической мысли? Ну давай посмотрим насколько оно хорошо...

Как примерно должен выглядеть интерфейс актора на с/c++

struct Actor {
    virtual ~Actor() = default;
    virtual void Receive(std::unique_ptr<Event> ev) noexcept = 0;
    // и небольшое кол-во методов и объектов окружения актора 
};
вот и всё. За кадром остаются схемы бутстрапа и уничтожения актора, кол-во метаданных объектов ... и прочая реализационно зависимая мелочёвка. Сам актор имеет ящик N к 1 и ему всегда гарантированно доходят все сообщения в пределах времени жизни в Receive(...). Это самый простой и универсальный интерфейс на котором можно эффективно приготовить что угодно, хоть варианты подписок NxM с разными семантиками (pull или push схемы, агрегацию состояний, таймауты и т.д.). И внутри которого можно наделать какие угодно диспатчеры сообщений - хочешь, делай стейты на плюсовом сахаре, а не хочешь - не длеай.

А в SO получилась неуклюжая калька теоретических статей по акторным моделям бездумно положенная на с++ сахар. Тут подразумевается вся эта мудотня с подписками и стейтами в so_define_agent(...) - как, кстати, можно подписаться вообще на все сообщения? И что происходит с отправленными сообщенями, но на которые нет подписок? Похоже, что они тупо теряются и это огромнейший недостаток дизайна библиотеки т.к. выискивать подобные нарушения контрактов в сложных приложениях крайне проблематично.

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

Искусство инженера это делать сложные вещи максимально просто, но SO не является предметом такого искусства. Можно работать десятки лет и получить посредственный продукт. Потраченные десятки лет на посредственный продукт едва ли могут давать основание считать себя лучше многих произвольно взятых людей.

anonymous ()
Ответ на: комментарий от anonymous

Что это тут такое ...

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

Вот, например, RazrFalcon. Умеет писать код, но за пределами Rust-а он чаще всего проявляет себя идиотом. Или den73, который настолько подвинут Lisp-ами, что в остальном оказывается полным неадекватом. Про множественные инкарнации Царя можно даже не говорить.

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

А вот у вас, как и у любого другого анонима, этого нет. Любой анонимный комментарий заставляет задумываться: разумен ли человек, который его написал, знает и умеет ли он хоть что-нибудь? Или это очередное воплощение Царя, anonimusa или это какая-нибудь школота развлекается.

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

Вот поэтому любые анонимные эксперты — это «никто и звать никак». Не нравится вам быть «никем» — заведите себе аккаунт. Тогда с вами будут общаться именно как с вами. Пока вы этого не сделали — вы никто. Вообще.

прёт на ружу комплекс неполноценности быдлядской душонки считающей своё жалкое поделие вершиной технической мысли?

Вы у меня в голове успели покопаться? Может диагнозы по Интернету — это ваша профессия?

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

Продолжаем ставить диагнозы по Интернету? Ожидаемо.

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

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

eao197 ★★★★★ ()
Ответ на: комментарий от anonymous

Как примерно должен выглядеть интерфейс актора на с/c++

Сразу фейл. Кому «должен»? Может выглядеть так. А может так и не выглядеть. И, судя по опыту, лучше, если так не будет выглядеть. Но для этого нужно иметь опыт разработки, а не гавканья на LOR-е из под анонимуса.

virtual void Receive(std::unique_ptr<Event> ev) noexcept
Это самый простой и универсальный интерфейс на котором можно эффективно приготовить что угодно, хоть варианты подписок NxM с разными семантиками (pull или push схемы, агрегацию состояний, таймауты и т.д.).

И еще раз фейл. Да еще и не один. Кто там звиздел на счет базвордов? Вот вы тут кучу базвордов и перечислили нихера не понимая. Особенно доставило про pull и push.

Про модель N:M в сочетании с std::unique_ptr<Event> вы знатно перданули. Поскольку передача сообщения посредством unique_ptr будет работать разве что для N:1. А в модели N:M вам придется иметь либо unique_ptr<shared_ptr<Event>>, либо же делать специальный Event, внутри которого уже будет shared_ptr. И клонировать этот самый Event для каждого получателя. Отличное решение, браво.

// и небольшое кол-во методов и объектов окружения актора
За кадром остаются схемы бутстрапа и уничтожения актора, кол-во метаданных объектов ... и прочая реализационно зависимая мелочёвка.

Еще один признак типичного теоретика. Вы вот возьмите и сделайте все это, протестируйте и опробуйте в нескольких разных проектах. Хотя бы самостоятельно. Не говоря уже о том, чтобы ваше творение попробовали разные люди, с разными взглядами на то, как нужно программировать. Тогда желание надувать щеки и отделываться фразами «и прочая реализационно зависимая мелочёвка» пройдет само собой. Только это ведь делать что-то нужно, а не анонимно лужи на LOR-е газифицировать.

Сам актор имеет ящик N к 1 и ему всегда гарантированно доходят все сообщения в пределах времени жизни в Receive(...).

Вы хоть отдаете себе отчет о том, что значит фраза «всегда гарантированно доходят»?

Это самый простой и универсальный интерфейс на котором можно эффективно приготовить что угодно,

И кто это все будет готовить? Вася Пупкин, которому достанется ваше творение, где у актора будет только один метод Receive?

Когда у вас в приложении пять акторов, у каждого из них всего одно состояние, и каждый обрабатывает не более двух-трех разных типов сообщений, то можно довольствоваться и такими убогими акторами. Хотя при этом возникает логичный вопрос: а нахера там вообще акторы, если можно обойтись каким-нибудь примитивным thread-pool-ом, thread-safe message queue и, может быть, каким-то зачаточным механизмом task-ов.

А вот если бы вы имели дело с реальными акторами, да которые еще и развиваются в течении нескольких лет, то один-единственный Receive, в котором самому разработчику приходится разбираться с состояниями актора и различными типами сообщений, то вам бы это быстро надоело бы. Ибо именно так и происходит (см. здесь). И именно поэтому для Akka появляется Akka.FSM, а для Erlang-а gen_statem.

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

Просто так не получится. Чтобы можно было делать какие угодно диспетчеры, нужно жестко разделять различные понятия, например, актор — отдельно, диспетчер — отдельно, mailbox-ы и event-queue отдельно. И, более того, даже этого будет недостаточно. Нам, например, для отдельных случаев пришлось сделать особую штуку, как env_infrastructure.

А в SO получилась неуклюжая калька теоретических статей по акторным моделям бездумно положенная на с++ сахар.

Интересно было бы увидеть ссылки на те теоретические статьи, кальку с которых мы делали.

Тут подразумевается вся эта мудотня с подписками и стейтами в so_define_agent(...)

Эта мудотня происходит из ряда особенностей, продиктованных практическими соображениями.

как, кстати, можно подписаться вообще на все сообщения?

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

И что происходит с отправленными сообщенями, но на которые нет подписок?

Они до агента не доходят. В зависимости от типа mbox-а, в который сообщение отсылалось, оно может быть либо выброшено сразу, внутри send-а, либо же впоследствии, при поиске подписки.

Похоже, что они тупо теряются и это огромнейший недостаток дизайна библиотеки

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

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

Проблема нарушения контракта не в том, что вы не можете получить сообщение, на которое не подписались. А в том, что сами mailbox-ы являются нетипизированными. Более того, одних только типизированных mailbox-ов для строгого выражения контрактов недостаточно, т.к. видимый интерфейс mailbox-а должен меняться в зависимости в от того, в какой стадии находится взаимодействие между акторами. Скажем, если акторы A и B обмениваются сообщениями m1, m2, m3, причем в такой последовательности:

A -m1-> B
A <-m2- B
A -m3-> B
то mailbox сперва должен разрешать отсылку m1 от A к B, затем отсылку m2 от B к A, затем m3 от A к B.

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

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

Неуклюжая и переинженеренная - мейлбоксы с множественной подпиской (которые MPMC), являющиеся нишевой фичей которые пилят кастомно под задачу, засунуты в ядро библиотеки.

MPMC-mbox-ы — это как раз заимствование из модели Pub/Sub. И это более чем отлично работает для определенного класса задач. И они засунуты в библиотеку сразу для того, чтобы пользователям не приходилось пилить свои лисапеды.

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

Это объяснимо происхождением SObjectizer-а, который родился из задач АСУТП, а там Pub/Sub применяется весьма широко.

Искусство инженера это делать сложные вещи максимально просто, но SO не является предметом такого искусства.

А где-то утверждалось, что SObjectizer — это предмет искусства?

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

Я вам больше скажу, для каких-то задач SObjectizer в принципе не подойдет. И столкнувшись с такими задачами пользователю придется либо брать другой инструмент (их для C++ немного, но они есть), либо делать что-то свое, сильно специализированное. Но для каких-то задач подойдет вполне. И чем клепать собственные надстройки над минималистичными акторами, у которых есть только Receive, можно сразу взять SObjectizer и сэкономить себе кучу сил и времени. Даже если SObjectizer будет использован только для прототипирования.

eao197 ★★★★★ ()

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

eao197 ★★★★★ ()

Мы подготовили статью, в которой постарались рассказать (и показать на картинках) про основные сущности, которые есть в SObjectizer, а так же про взаимосвязи и взаимодействие этих сущностей: Давайте заглянем SObjectizer-у под капот.

eao197 ★★★★★ ()
Ответ на: комментарий от Dark_SavanT

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

Впрочем, это наши личные заморочки. По ряду причин мы ведем разработку SO-5 на SF, для тех, кому SF не нравится, есть зеркало на github-е.

eao197 ★★★★★ ()

Отсутсвие, на 2015г., полной поддержки на целевой платформе (QNX6.4) С++11.

Для компиляции не хватило, как мимимум, «std::chrono::steady_clock».

anonymous ()
Ответ на: комментарий от anonymous

QNX — это специфическая платформа, с конца 90-х не приходилось с ней сталкиваться. Так что мы на нее даже и не замахивались. И, наверное, даже не знали, что кто-то пытается на QNX попробовать SO-5.

eao197 ★★★★★ ()

Очередная статья про SObjectizer: Обмен информацией между рабочими нитям без боли? CSP-шные каналы нам в помощь.

На этот раз мы знакомим читателя с программированием без агентов/акторов, но с использованием CSP-шных каналов, которые в SObjectizer-е называются mchain-ы.

Если не будет дополнительных «вбрасываний» от читателей, то следующая запланированная статья расскажет о том, как можно построить транспорт на базе SObjectizer-овских mbox-ов и MQTT-брокера.

Кто не любит Хабр, тот может сказать свое «фи» оставить свой комментарий здесь.

eao197 ★★★★★ ()

Новая большая статья: Добавляем распределенность в SObjectizer-5 с помощью MQTT и libmosquitto

Две просьбы к тем читателям, которые заинтересуются.

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

Во-вторых, если кому-то нужен вариант этой статьи на рунглише с закосом на инглиш, то дайте знать.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Кек, хабраинвалид, а от того что кармочки нет тоже страдаешь, нааерное? По-тихому не поднасрать и ротик не заткнуть как это там у вас в ТМ-клоаке принято?

anonymous ()

В августе на митапе в Питере я сделал доклад «Акторы в C++: взгляд старого практикующего актородела». Там немного рассказал про свой взгляд на Модель Акторов и о том, применима ли она в C++ вообще. А так же попробовал коротко познакомить слушателей с SObjectizer-ом.

Видео доклада доступно на YouTube: https://www.youtube.com/watch?v=c1qSVSHoMjU

Презентацию в виде PDF можно взять здесь, либо можно просмотреть ее на SlideShare или на Google Docs.

eao197 ★★★★★ ()
Последнее исправление: eao197 (всего исправлений: 1)

Что войдет в версию 5.5.23

Мы обозначили набор возможных фич для следующей версии 5.5.23, работа над которой уже идет. На плохом английском этот перечень изложен здесь: https://sourceforge.net/p/sobjectizer/blog/2018/09/what-can-or-should-go-into...

Для тех, кто не хочет читать рунглиш, краткое изложение:

  • добавить в SO-5.5 какие-то средства для адаптивной балансировки нагрузки (т.е. как-то добавить в SO-5.5 кроме «push»-модели еще и «pull»-модель);
  • возможно, при этом еще и получится заиметь простую интеграцию mchain-ов и агентов. Т.е. сообщение отсылается в mchain, а связанный с этим mchain-ом агент получает сообщение обычным образом;
  • гарантированная отмена таймеров. Ибо сейчас если при отмене таймера сообщение уже стоит в очереди агента, то до агента сообщение все-таки дойдет;
  • возможно при реализации отмены таймеров получится еще и заиметь механизм отмены/отзыва отосланных ранее сообщений. Или сообщений, надобность обработки которых меняется со временем;
  • функционал, аналогичный ReceiveTimeout из Akka. Т.е. если какой-то актор сидит без дела некоторое время, то ему прилетает специальное уведомление;
  • какой-то инструментарий для unit-тестирования агентов.

Можно высказываться как по поводу того, что было обозначено нами (вроде нужно/не нужно). Так и предложить то, чего лично вам не хватает. Если не удобно оставлять комментарии на SF.net, то это можно сделать здесь.

Еще мы хотим подготовить и опубликовать на Хабре очередную статью. Пока предполагаемая тема — это разбор примера intercom_statechart, в котором иерархические конечные автоматы используются на полную катушку. Но если кому-то интересно что-то другое, то обозначайте. Постараемся раскрыть интересную вам тему. Либо в ближайшей статье, либо в одной из последующих.

eao197 ★★★★★ ()

Статья про иерархические конечные автоматы и SO-5.5

Мы подготовили очередную статью с рассказом про возможности SObjectizer. Эта статья может быть интересна не только тем, кто интересуется SObjectizer-ом, но и тем, кто раньше не сталкивался с иерархическими конечными автоматами. Первая часть статьи рассказывает именно про ИКА и их продвинутые возможности.

eao197 ★★★★★ ()

Приблизительный роадмап на 2018-2019

Вот здесь изложено что-то вроде плана работ над SObjectizer-5 на 2018 и 2019-й.

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

eao197 ★★★★★ ()

SO-5.5.23-beta1 и so_5_extra-1.2.0-beta1

Сегодня были зафиксированы первые бета-версии наших проектов SObjectizer и so_5_extra. Загрузить их можно отсюда: so-5.5.23-beta1.zip и so_5_extra-1.2.0-beta1.zip (либо so_5_extra-1.2.0-beta1-full.zip).

Подробнее об нововведениях рассказывается в очередной статье на Хабре.

Со сроками официально релиза пока так: ориентировочно релиз состоится в первой декаде ноября. Если успеем раньше, выкатим раньше. Но там еще много работы, в том числе и по документированию, и по проверке сборки под Android с помощью Google-овского NDK и еще разных мелочей (и не мелочей). Так что первая декада ноября выглядит более реалистично.

В общем, если кому-то интересно, что в SObjectizer-е происходит, то смотрим, делимся впечатлениями и соображениями. Пока еще есть время и возможность повлиять на то, что попадет в SO-5.5.23 и so_5_extra-1.2.0.

eao197 ★★★★★ ()