Выходит, если нельзя использовать внутри on_enter/on_exit без try/catch (как вы делаете у себя в примере).
Методы on_enter/on_exit лучше всего рассматривать как некоторые аналоги конструкторов и деструкторов, поскольку у них:
во-первых, очень специфические задачи (on_enter похож на конструктор, т.к. позволяет задать какие-то начальные значения, а on_exit похож на деструктор, т.к. позволяет освободить ресурсы);
во-вторых, они выполняются в очень специфическом контексте, в котором возможности самого SObjectizer-а по преодолению возникающих проблем крайне ограничены. Очень похоже на ситуацию с выбросом исключения из деструктора объекта — как правило тут уж ничего кроме std::terminate не сделать.
Поэтому если есть необходимость запихнуть в on_enter/on_exit какие-то сложные действия с высокой вероятностью возникновении ошибки, то нужно обрамлять эти действия блоком try/catch и самостоятельно ошибку устранять.
Но тут возникает другой момент: допустим, в on_enter вам нужно сделать действия A, B и C, и на действии C у вас возникает ошибка. Что делать в этом случае? Вы не может остаться в новом состоянии, т.к. из подготовительных действий A, B и C вы сделали только A и B. Но и просто откатить A и B так же недостаточно, т.к. вы не может просто так вернуться в свое исходное состояние.
В общем, куда не кинь, везде клин :(
Поэтому мы исходим из следующих соображений:
когда при переходе из состояния в состояние нужно делать какие-то сложные цепочки действий с высокой вероятностью ошибок, то это следует делать не в on_enter/on_exit, а непосредственно в коде агента перед вызовом so_change_state;
на долю on_enter/on_exit остаются самые тривиальные действия, вроде назначения начальных значений. Если уж эти действия приводят к ошибкам, то лучше уж вызвать std::terminate и рестартовать приложение, чем пытаться выбраться из такой ситуации.
Как мне кажется, за зацикливанием должен следить все таки пользователь библиотеки.
В условиях иерархических конечных автоматов и, особенно, наследования, следить может быть слишком сложно. Т.е. вы написали класс агента A с несколькими состояниями, затем ваш коллега отнаследовался от A и ввел еще несколько состояний. Ваш коллега вообще может не знать, что в каком-то своем on_enter/on_exit вы делаете еще одну смену состояния. Так же как и вы не можете знать, что будет происходить в on_enter/on_exit у наследников вашего класса.
А при таких раскладах приходится слать дополнительное, совершенно не нужное, сообщение для смены состояния.
А расскажите, пожалуйста, про ваш случай подробнее. Может мы и правда слишком жестко к ограничениям относимся.
Кстати, на счет noexept, so_5::send ведь не noexept?
Было бы не плохо этот момент хотя бы немного осветить в этой статье.
Да, наверное, вы правы. Какие-то вещи представляются очевидными мне самому, но они не обязаны быть таковыми для всех остальных.
Если разрешить переходы в другое состояние в on_enter/on_exit, то легко дойти до ситуаций, когда возникнет зацикливание. И тогда следов проблемы не найдешь.
Еще один не самый очевидный момент: действия в on_enter/on_exit должны быть noexcept. Поскольку если при смене состояния (особенно если эта смена происходит в сложном иерархическом автомате) возникает исключение, то откатить все к исходной точке и обеспечить strong exception guarantee, скорее всего не получится.
Если хоть кому-то нравится, то значит не напрасно все было. Мы, кстати, открыты: есть интересно о чем-то еще узнать, то скажите о чем, постараемся выбрать время и рассказать.
Итого, выходит что разница между обычным агентом и stage-агентам только во времени жизни.
На самом деле не только. Разница в сроке жизни это уже следствие. Принципиальный момент в том, что stage-агент должен уметь обрабатывать операции сразу для N транзакций. Это усложняет его реализацию.
Ведь если email_body_checker проверяет только одно тело сообщения, то его реализация будет достаточно простой. А вот если он может _одновременно_ проверять сразу несколько сообщений, то это уже совсем другое дело.
Хотя email_body_checker — это не самый хороший здесь пример. Можно взять пример агента, который что-то с СУБД делает. Например, пусть в каждом email-е есть уникальный message-id и нам нужно сохранять эти message-id в БД для истории. Если агент message_id_saver рассчитан только на один email, то у него все просто: ему дали message-id, он получил коннект к БД, выполнил сохранение, отчитался о выполнении.
Но если мы будем создавать сразу 100500 таких простых message_id_saver, то наша работа не будет хорошо масштабироваться. Т.к. все эти агенты будут конкурировать за доступ к БД. И выполнять сохранение данных в БД построчно, что так же не эффективно.
А вот если мы сделаем stage-агента mass_message_id_saver, который может принять кучу message-id и все их вставить в БД одной bulk-операцией, то масштабирование у нас будет лучше. Но сама логика mass_message_id_saver станет сложнее, т.к. он должен быть накапливать у себя группу message-id, затем делать bulk-операцию, затем должен уметь обрабатывать негативные результаты buld-операции (например, что делать если из M message-id один все-таки оказался неуникальным?), должен уметь раздавать результаты разным агентам-инициаторам и т.д. и т.п.
В итоге получается, что каждый stage-агент оказывается заметно сложнее, чем простые агенты вроде email_body_checker. Но приложение из stage-агентов собрать проще и следить за ним проще, чем в случае кучи простых мелких агентов.
Эдакий синглтон.
На самом деле для балансировки нагрузки можно сделать сразу несколько однотипных stage-агентов для одной и той же операции. Но это уже совсем другая история ;)
Странно, что к вашим статьям так мало комментариев.
Видимо, скучные и не формат.
Но почему же тогда они не являются stage-агентами?
Если я правильно понимаю staging из SEDA, то stage-компонент (агент) выполняет одну и ту же операцию для разных транзакций. Соответственно, stage-компонент живет дольше, чем одна конкретная транзакция.
У меня же в предыдущей статье агенты вроде email_headers_checker и email_body_checker выполняют всего одну операцию всего для одной транзакции. Соответственно, когда конкретный агент email_headers_checker свою работу для своей транзакции завершает, он уничтожается, а эту же операцию для другой транзакции будет делать другой экземпляр email_header_checker.
Если бы email_header_checker был stage-агентом, он бы выполнял свою операцию для разных транзакций и не исчезал бы после обслуживания очередной транзакции.
А вот на счет возврата vector как результат функции, ведь в этом случае произойдет копирование вектора.
Здесь сработает NRVO-оптимизация, когда возвращаемое значение будет создано на стеке в вызывающей функции, а вызываемая функция будет сразу работать с этим значением.
Подскажите пожалуйста чем он удобнее в защите от исключений?
У вас может быть так:
std::vector<string> responses;
try {
MakeSspdRequest(responses);
} catch(...) { ... }
for(const auto & r : responses) // В каком состоянии сейчас responses?
Если у вас вот такой код:
try {
auto responses = MakeSspdRequest();
...
} catch(...) {...}
То в случае исключения у вас responses просто не останется и не возникнет вопросов о том, что там внутри находится.
Такой подход гораздо удобнее и в плане защиты от исключений, и в плане простоты использования.
В C++ редко доводиться видеть конструкции вида:
using std::string;
using std::vector;
using std::cin;
using std::cout;
using std::endl;
using Poco::Net::SocketAddress;
using Poco::Net::DatagramSocket;
using Poco::Timespan;
using Poco::RegularExpression;
Обычно довольствуются using namespace, особенно в таких коротких программах примерах:
using namespace std;
using namespace Poco;
using namespace Poco::Net;
то необходимо реализовать лишь интерпретацию этого формализма в виде программной или аппаратной реализации
Собственно, тогда вообще два вопроса возникают:
1. Чем «SC-код» отличается от уже известных моделей вроде Actor Model или CSP? И почему именно SC-код был нужен.
2. Даже если вам нужна была реализация SC-кода, то вы могли делать ее не с нуля, а взяв за основу что-то готовое. Тем самым минимизировав затраты на разработку все системы. Поскольку какие-то вещи, вроде поддержки многопоточности, вы бы переиспользовали, а не создавали заново. Отсюда и вопрос: почему не было взято-то что-то готовое?
Ну и где-то на верхнем уровне, где нужно было бы с исключениями разбираться для показа их пользователю, нужно было бы сделать соответствующую реализацию error_visitor-а.
Т.е. суть в том, чтобы ловить исключение там, где его ловля актуальна и где требуется его преобразование в app_exception. Затем вытаскивание максимального количества полезной информации из самого исключения и окружающего его контекста, запихивание всей этой информации в какой-то объект (наследник error_case) и передача вверх уже этого объекта, а не имеющего конкретного типа exception_ptr.
PS. Все это выглядит слишком олскульно и имеет запах пропавших нафталином паттернов от банды четырех, но в отсуствии АлгТД это вполне себе работающий подход.
Второе. Ведь в месте, где вы ловите первое исключение, у вас есть не только само исключение, но и описание контекста, в котором оно возникло (например, ID запроса, параметры сбойной операции и т.д.). Соответственно, в «стек» можно поместить не только исключение, но и описание контекста.
Тем более, по вашим собственным словам, его присутствие в нише системного программирования минимальное.
На данный момент так и есть, со временем ситуация может измениться. Тогда можно будет посмотреть, насколько широко в системном программировании будут применяться сложные составные Rust-овские enum-ы.
Но, опять же, единственный способ вытрясти информацию о вложенном исключении
Тут интереснее другое — зачем нужно выстраивать цепочку вложенных исключений.
К примеру, в iostreams ошибки репортятся через состояние объекта.
Корни iostreams восходят к временам, когда исключений в C++ еще не было. Кроме того, со временем в iostreams добавили возможность включить бросание исключений в случае ошибки.
filesystem имеет отдельный набор перегрузок, которые возвращают код ошибки.
А это как раз следствие того, что хотели угодить всем: и тем, кого исключения устраивают, и тем, кому исключения не нравятся.
Это при том, что С++ частенько используют в системном программировании, где исключения отключены.
Для системного программирования альтернатива в виде Rust-а появилась совсем недавно. Да и присутствие в этой нише того же Rust-а пока минимальное.
На счет корявости раскрутки исключений. Есть ощущение, что коряво это выглядит так, потому, что мало кому нужно использовать исключения именно таким образом.
Накидали — это громко сказано. Коды возврата пришли из C. Исключения появились вскоре после C++ 1.0. И именно наличие исключений сделало возможным нормальное применение перегруженных операторов. Для конца 1980-х и начала 1990-х было вполне себе нормально.
Другое дело, что сейчас модно и молодежно в АглТД и паттерн-матчинг. А вот этого в C++ нет и, к сожалению, вряд ли появится в ближайшие лет 5-6, а то и вообще вряд ли.
В модели с исключениями вы этого не увидите вообще никак
В модели с исключениями меня, как правило, вообще не интересует, какое именно исключение вылетит и почему. Мне нужно знать всего лишь может ли вылететь исключение или нет. Если функция/метод/оператор не помечен как noexcept, то исключение вылететь может (т.е. может вылететь практически отовсюду, если только речь не идет про хардкорный embedded или жесткий real-time). Поэтому единственное, что меня заботит — это сохранение инвариантов и откат операций. А для этого в современном C++ есть достаточно средств.
Но у вас явно другая специфика, раз вам нужно понимать что именно выскочило.
Касательно вашего примера с исключениями. Не суть понятно, зачем вам хранить всю цепочку исключений. Но создается ощущение, что если вам нужна именно вся цепочка ошибок, то в C++ без алгебраических типов и паттерн-матчинга вы все равно поимеете приключения. Даже если придумаете хитрый возвращаемый тип result<R,E>, где в качестве E будет выступать что-то, что способно хранить цепочку вложенных ошибок.
Методы on_enter/on_exit лучше всего рассматривать как некоторые аналоги конструкторов и деструкторов, поскольку у них:
Поэтому если есть необходимость запихнуть в on_enter/on_exit какие-то сложные действия с высокой вероятностью возникновении ошибки, то нужно обрамлять эти действия блоком try/catch и самостоятельно ошибку устранять.
Но тут возникает другой момент: допустим, в on_enter вам нужно сделать действия A, B и C, и на действии C у вас возникает ошибка. Что делать в этом случае? Вы не может остаться в новом состоянии, т.к. из подготовительных действий A, B и C вы сделали только A и B. Но и просто откатить A и B так же недостаточно, т.к. вы не может просто так вернуться в свое исходное состояние.
В общем, куда не кинь, везде клин :(
Поэтому мы исходим из следующих соображений:
А расскажите, пожалуйста, про ваш случай подробнее. Может мы и правда слишком жестко к ограничениям относимся.
Не noexcept, send может бросать исключения.
Если разрешить переходы в другое состояние в on_enter/on_exit, то легко дойти до ситуаций, когда возникнет зацикливание. И тогда следов проблемы не найдешь.
Еще один не самый очевидный момент: действия в on_enter/on_exit должны быть noexcept. Поскольку если при смене состояния (особенно если эта смена происходит в сложном иерархическом автомате) возникает исключение, то откатить все к исходной точке и обеспечить strong exception guarantee, скорее всего не получится.
На самом деле не только. Разница в сроке жизни это уже следствие. Принципиальный момент в том, что stage-агент должен уметь обрабатывать операции сразу для N транзакций. Это усложняет его реализацию.
Ведь если email_body_checker проверяет только одно тело сообщения, то его реализация будет достаточно простой. А вот если он может _одновременно_ проверять сразу несколько сообщений, то это уже совсем другое дело.
Хотя email_body_checker — это не самый хороший здесь пример. Можно взять пример агента, который что-то с СУБД делает. Например, пусть в каждом email-е есть уникальный message-id и нам нужно сохранять эти message-id в БД для истории. Если агент message_id_saver рассчитан только на один email, то у него все просто: ему дали message-id, он получил коннект к БД, выполнил сохранение, отчитался о выполнении.
Но если мы будем создавать сразу 100500 таких простых message_id_saver, то наша работа не будет хорошо масштабироваться. Т.к. все эти агенты будут конкурировать за доступ к БД. И выполнять сохранение данных в БД построчно, что так же не эффективно.
А вот если мы сделаем stage-агента mass_message_id_saver, который может принять кучу message-id и все их вставить в БД одной bulk-операцией, то масштабирование у нас будет лучше. Но сама логика mass_message_id_saver станет сложнее, т.к. он должен быть накапливать у себя группу message-id, затем делать bulk-операцию, затем должен уметь обрабатывать негативные результаты buld-операции (например, что делать если из M message-id один все-таки оказался неуникальным?), должен уметь раздавать результаты разным агентам-инициаторам и т.д. и т.п.
В итоге получается, что каждый stage-агент оказывается заметно сложнее, чем простые агенты вроде email_body_checker. Но приложение из stage-агентов собрать проще и следить за ним проще, чем в случае кучи простых мелких агентов.
На самом деле для балансировки нагрузки можно сделать сразу несколько однотипных stage-агентов для одной и той же операции. Но это уже совсем другая история ;)
Если я правильно понимаю staging из SEDA, то stage-компонент (агент) выполняет одну и ту же операцию для разных транзакций. Соответственно, stage-компонент живет дольше, чем одна конкретная транзакция.
У меня же в предыдущей статье агенты вроде email_headers_checker и email_body_checker выполняют всего одну операцию всего для одной транзакции. Соответственно, когда конкретный агент email_headers_checker свою работу для своей транзакции завершает, он уничтожается, а эту же операцию для другой транзакции будет делать другой экземпляр email_header_checker.
Если бы email_header_checker был stage-агентом, он бы выполнял свою операцию для разных транзакций и не исчезал бы после обслуживания очередной транзакции.
Здесь сработает NRVO-оптимизация, когда возвращаемое значение будет создано на стеке в вызывающей функции, а вызываемая функция будет сразу работать с этим значением.
У вас может быть так:
Если у вас вот такой код:
То в случае исключения у вас responses просто не останется и не возникнет вопросов о том, что там внутри находится.
Исключения не следует ловить по значению, т.к. может произойти «срезка» объекта. Лучше ловить по константной ссылке:
Вот здесь вам не нужна копия IP-адреса для печати:
Лучше брать ссылку на очередное значение, причем константную ссылку дабы подчеркнуть, что ничего изменяться не будет:
Аналогично, полагаю, имеет смысл сделать и с циклом по responses.
В C++11 и выше функцию MakeSsdpRequest имеет смысл переписать так, чтобы она возвращала вектор, а не получала его по неконстантной ссылке:
Что позволит использовать ее вот так:
Такой подход гораздо удобнее и в плане защиты от исключений, и в плане простоты использования.
В C++ редко доводиться видеть конструкции вида:
Обычно довольствуются using namespace, особенно в таких коротких программах примерах:
Собственно, тогда вообще два вопроса возникают:
1. Чем «SC-код» отличается от уже известных моделей вроде Actor Model или CSP? И почему именно SC-код был нужен.
2. Даже если вам нужна была реализация SC-кода, то вы могли делать ее не с нуля, а взяв за основу что-то готовое. Тем самым минимизировав затраты на разработку все системы. Поскольку какие-то вещи, вроде поддержки многопоточности, вы бы переиспользовали, а не создавали заново. Отсюда и вопрос: почему не было взято-то что-то готовое?
Потом бы в местах, где я ловлю исключения и преобразую их в app_exception делал бы что-то вроде:
Ну и где-то на верхнем уровне, где нужно было бы с исключениями разбираться для показа их пользователю, нужно было бы сделать соответствующую реализацию error_visitor-а.
Т.е. суть в том, чтобы ловить исключение там, где его ловля актуальна и где требуется его преобразование в app_exception. Затем вытаскивание максимального количества полезной информации из самого исключения и окружающего его контекста, запихивание всей этой информации в какой-то объект (наследник error_case) и передача вверх уже этого объекта, а не имеющего конкретного типа exception_ptr.
PS. Все это выглядит слишком олскульно и имеет запах пропавших нафталином паттернов от банды четырех, но в отсуствии АлгТД это вполне себе работающий подход.
Тут интереснее другое — зачем нужно выстраивать цепочку вложенных исключений.
А это как раз следствие того, что хотели угодить всем: и тем, кого исключения устраивают, и тем, кому исключения не нравятся.
Для системного программирования альтернатива в виде Rust-а появилась совсем недавно. Да и присутствие в этой нише того же Rust-а пока минимальное.
На счет корявости раскрутки исключений. Есть ощущение, что коряво это выглядит так, потому, что мало кому нужно использовать исключения именно таким образом.
Другое дело, что сейчас модно и молодежно в АглТД и паттерн-матчинг. А вот этого в C++ нет и, к сожалению, вряд ли появится в ближайшие лет 5-6, а то и вообще вряд ли.
Но у вас явно другая специфика, раз вам нужно понимать что именно выскочило.
в котором обращение к get_base() и get_seed() может приводить к преждевременному возврату с каким-то значением Error.
Касательно вашего примера с исключениями. Не суть понятно, зачем вам хранить всю цепочку исключений. Но создается ощущение, что если вам нужна именно вся цепочка ошибок, то в C++ без алгебраических типов и паттерн-матчинга вы все равно поимеете приключения. Даже если придумаете хитрый возвращаемый тип result<R,E>, где в качестве E будет выступать что-то, что способно хранить цепочку вложенных ошибок.