All streams
Search
Write a publication
Pull to refresh
82
0
Евгений Охотников @eao197

Велосипедостроитель, программист-камикадзе

Send message
Я пока прокомментирую вот это:
Выходит, если нельзя использовать внутри 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?
Не noexcept, send может бросать исключения.
Было бы не плохо этот момент хотя бы немного осветить в этой статье.
Да, наверное, вы правы. Какие-то вещи представляются очевидными мне самому, но они не обязаны быть таковыми для всех остальных.

Если разрешить переходы в другое состояние в 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++ еще только изучаете, то еще несколько советов:

Исключения не следует ловить по значению, т.к. может произойти «срезка» объекта. Лучше ловить по константной ссылке:
catch (const Poco::TimeoutException &) { }


Вот здесь вам не нужна копия IP-адреса для печати:
for (string ip : ips) {
		cout << "IP: " << ip << endl;
	}

Лучше брать ссылку на очередное значение, причем константную ссылку дабы подчеркнуть, что ничего изменяться не будет:
for (const string & ip : ips) {
		cout << "IP: " << ip << endl;
	}

Аналогично, полагаю, имеет смысл сделать и с циклом по responses.

В C++11 и выше функцию MakeSsdpRequest имеет смысл переписать так, чтобы она возвращала вектор, а не получала его по неконстантной ссылке:
vector<string> MakeSsdpRequest(string st = "") {
  vector<string> responses;
  ...
  return responses;

Что позволит использовать ее вот так:
int main()
{
	auto responses = MakeSsdpRequest();

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

В 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-кода, то вы могли делать ее не с нуля, а взяв за основу что-то готовое. Тем самым минимизировав затраты на разработку все системы. Поскольку какие-то вещи, вроде поддержки многопоточности, вы бы переиспользовали, а не создавали заново. Отсюда и вопрос: почему не было взято-то что-то готовое?
Интересно, а почему вы начали делать свою реализацию агентов на C++, а не взяли какую-то из уже существующих реализаций той же Модели Акторов для C++?
А слайды докладов и, может быть, видео выступлений затем будут доступны?
Возможно, я бы даже не хранил бы исходное исключение. Может быть что-то вроде:
class error_visitor;
class error_case {
public:
  virtual ~error_case();
  virtual void apply(error_visitor &) = 0;
};
class first_error_case : public error_case {
public:
  first_error_case(.../* some args*/) {...}
  virtual void apply(error_visitor & v) override { v.visit(*this); }
  ...
};
...
class error_visitor {
public :
  virtual void visit(const first_error_case &) = 0;
  virtual void visit(const second_error_case &) = 0;
  ...
};
class app_exception : public std::runtime_error {
public :
  ...
  const std::vector<std::unique_ptr<error_case>> & cases() const;
private :
  std::vector<std::unique_ptr<error_case>> cases_;
};
Потом бы в местах, где я ловлю исключения и преобразую их в app_exception делал бы что-то вроде:
void some_internal_function() {
  try {
    ... /* some actions */
  }
  catch(const first_exception_type & x) {
    throw app_exception(..., std::make_unique<first_error_case>(...));
  }
  catch(const second_exception_type &x) {
    throw app_exception(..., std::make_unique<second_error_case>(...));
  }
  ...
}
Ну и где-то на верхнем уровне, где нужно было бы с исключениями разбираться для показа их пользователю, нужно было бы сделать соответствующую реализацию 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++ есть достаточно средств.

Но у вас явно другая специфика, раз вам нужно понимать что именно выскочило.
Но без short-circuit return в выражениях оно будет выглядеть убого.
Спорный вопрос. Как по мне, так Rust-овский? только усложняет чтение кода (хотя, наверное, чтение упрощает). Не хотелось бы мне в C++ видеть код вроде
result<int, Error> some_action() {
  int i = get_base()? + get_seed()?;
  ...
}
в котором обращение к get_base() и get_seed() может приводить к преждевременному возврату с каким-то значением Error.
Это будет работать как выражение?
Нет, if в C++ — это statement, а не expression.

Касательно вашего примера с исключениями. Не суть понятно, зачем вам хранить всю цепочку исключений. Но создается ощущение, что если вам нужна именно вся цепочка ошибок, то в C++ без алгебраических типов и паттерн-матчинга вы все равно поимеете приключения. Даже если придумаете хитрый возвращаемый тип result<R,E>, где в качестве E будет выступать что-то, что способно хранить цепочку вложенных ошибок.

Information

Rating
5,336-th
Location
Гомель, Гомельская обл., Беларусь
Registered
Activity