Добрый день.
У меня возникла следующая проблема. Есть сопрограмма, внутри этой сопрограммы выбрасывается исключение, оно успешно разворачивает стек и покидает сопрограмму, после чего ловится в вызывавшем сопрограмму коде. Исключение кидается с помощью BOOST_THROW_EXCEPTION. Проблема в том, что в тестах это исключение не ловится. Я написал тесты с помощью boost test и вот там исключение не ловится вообще. В основном приложении все равботает, в тестах — нет, как такое может быть? Может boost test что-то делать, чтобы перехватывать исключения?
это невозможно по ряду причин, воспроизводится не везде, требует сложной сборки двух бинарников, в общем, я к опыту взываю, может кто просто сталкивался с подобным уже
Здравствуйте, chaotic-good, Вы писали:
A>>SSCCE или GTFO.
CG>это невозможно по ряду причин, воспроизводится не везде, требует сложной сборки двух бинарников, в общем, я к опыту взываю, может кто просто сталкивался с подобным уже
пальцем в небо — исключение пересекает границу so? У меня такая радость с gcc безо всяких корутин — исключение из so в главной проге не всегда ловится (с gcc3 проблем не было, началось с gcc4).
Здравствуйте, chaotic-good, Вы писали:
CG>Добрый день. CG>У меня возникла следующая проблема. Есть сопрограмма, внутри этой сопрограммы выбрасывается исключение, оно успешно разворачивает стек и покидает сопрограмму, после чего ловится в вызывавшем сопрограмму коде. Исключение кидается с помощью BOOST_THROW_EXCEPTION. Проблема в том, что в тестах это исключение не ловится. Я написал тесты с помощью boost test и вот там исключение не ловится вообще. В основном приложении все равботает, в тестах — нет, как такое может быть? Может boost test что-то делать, чтобы перехватывать исключения?
а кидаемые исключения от std::exception наследованы?
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Здравствуйте, chaotic-good, Вы писали:
CG>Добрый день. CG>У меня возникла следующая проблема. Есть сопрограмма, внутри этой сопрограммы выбрасывается исключение, оно успешно разворачивает стек и покидает сопрограмму, после чего ловится в вызывавшем сопрограмму коде. Исключение кидается с помощью BOOST_THROW_EXCEPTION. Проблема в том, что в тестах это исключение не ловится. Я написал тесты с помощью boost test и вот там исключение не ловится вообще. В основном приложении все равботает, в тестах — нет, как такое может быть? Может boost test что-то делать, чтобы перехватывать исключения?
Воообще странно, но вообще, кидать исключения в короутинах, оно наверно неправильно,
так как исключение обрабатывается дважды, для pull_type, например, сначала в исполнении калбека fn_:
param_type * from(
reinterpret_cast< param_type * >(
caller_->jump(
* callee_,
reinterpret_cast< intptr_t >( & to), /* <<<<<<<<<<<<<<<< Уход в контекст callee */
preserve_fpu() ) ) );
flags_ &= ~flag_running;
if ( from->do_unwind) throw forced_unwind();
if ( except_) rethrow_exception( except_); /* <<<<<<<<<<<<<<<< После возврата из callee повторное кидалово */
}
Оно надо? Что до исключений, то тут искать нужно не в boost coroutine, а в этих boost test, который может
опции компилятора какие ставит, при которых исключения вообще отключаются или какого ещё хлама они там напридумывали.
Здравствуйте, chaotic-good, Вы писали:
CG>Добрый день. CG>У меня возникла следующая проблема. Есть сопрограмма, внутри этой сопрограммы выбрасывается исключение, оно успешно разворачивает стек и покидает сопрограмму, после чего ловится в вызывавшем сопрограмму коде.
Вот это звучит очень подозрительно. Единственный правильный способ "покинуть" корутину — это прыгнуть обратно в вызывающий контекст. Соответственно, в корутине исключение должно быть поймано, контекст переключен и уже после переключения вызывающий контекст должен сам проверить, а был ли мальчик, то есть исключение.
Вообще, идея кидать исключения внтури корутин, а обрабатывать их в другом контексте, имхо, чаще всего несколько неправильная.
S>Воообще странно, но вообще, кидать исключения в короутинах, оно наверно неправильно, S>так как исключение обрабатывается дважды S>Оно надо?
Оно надо, ибо корутина — долгоживущая. Это часть сервера реализующая парсер протокола. В сокет прилетают данные не точно побитые на фреймы, они передаются в корутину и она парсит столько, сколько возможно, а потом возвращает управление если данных не хватает, потом получает новый буфер и продолжает парсить ну и тд. Вот как из этого всего вернуть ошибку правильнее всего, если не с помощью исключения?
S>ЗЫ boost-это мусорное ведро, если кто не в курсе.
За неимением альтернатив приходится использовать буст. В APR и glib не реализованы корутины и кое что еще, ну и asio мне нравится.
CG>У меня возникла следующая проблема. Есть сопрограмма, внутри этой сопрограммы выбрасывается исключение, оно успешно разворачивает стек и покидает сопрограмму, после чего ловится в вызывавшем сопрограмму коде. Исключение кидается с помощью BOOST_THROW_EXCEPTION. Проблема в том, что в тестах это исключение не ловится. Я написал тесты с помощью boost test и вот там исключение не ловится вообще. В основном приложении все равботает, в тестах — нет, как такое может быть? Может boost test что-то делать, чтобы перехватывать исключения?
проблема оказалась очень тупой, исключение выбрасывалось как надо, не проглатывалось, просто gdb тупил и брейкпоинт не срабатывал в обработчике исключения почему-то, сам обработчик при этом отрабатывал (это обнаружилось тупым отладочным выводом)
Здравствуйте, chaotic-good, Вы писали:
L>>Вообще, идея кидать исключения внтури корутин, а обрабатывать их в другом контексте, имхо, чаще всего несколько неправильная.
CG>Это один из юзкейсов корутин, описанный в документации.
Мне хитрая рыжая морда этого юзкейса очень не нравится.
Мой продукт, уже давно в продакшене на куче сайтов — тоже сетевой клиент. Сделан на первой версии boost::context, еще до того, как boost::coroutine официально оформилась. Я тоже смотрел на current_exception, но решил, что нуегонафиг.
Перевыброс исключения в другом контексте мне не нравится по двум причинам:
1. Теряешь оригинальный стек. Его, конечно, можно восстановить, но придется будить внутреннего мыщъх'а. Короче, удачной отладки крешдампа!
2. Размазываешь обработку ошибок почем зря
Последний тезис хочу развернуть. Вся суть использования корутин заключаетя в том, что ты пишешь асинхронный код в синхронном виде. Как-то вот так:
while (connection.up())
{
try
{
yield sendRequest();
ParseRequest();
ProcessRequest();
}
catch (const TCPConnectionException& Ex)
{
// connection lost, close socket and return from coroutine
}
catch (const ParseException& Ex)
{
// either re-try, skip, or close the connection and return from coroutine
}
}
То есть весь процесс спрятан внутри корутины, и единственный случай, когда происходит выход из нее — когда соединение закрыто. При этом все ошибки, связанные собственно с тем, что творилось в этом соединении, обрабатываются локально и не выпускаются наружу.
Попытка выпустить исключение для обработки в вызываемом контексте ИМХО убивает все преимущества использования корути на корню и запутывает код.
L>Мне хитрая рыжая морда этого юзкейса очень не нравится.
L>Мой продукт, уже давно в продакшене на куче сайтов — тоже сетевой клиент. Сделан на первой версии boost::context, еще до того, как boost::coroutine официально оформилась. Я тоже смотрел на current_exception, но решил, что нуегонафиг.
L>Перевыброс исключения в другом контексте мне не нравится по двум причинам: L>1. Теряешь оригинальный стек. Его, конечно, можно восстановить, но придется будить внутреннего мыщъх'а. Короче, удачной отладки крешдампа! L>2. Размазываешь обработку ошибок почем зря
Стек трейс ты теряешь даже без исключений, мне не очень нужен контекст когда я обрабатываю исключение, но если приложение падает, каким угодно образом, мне нужен стек трейс в дампе, но вот его я как раз не получу с корутинами.
L>То есть весь процесс спрятан внутри корутины, и единственный случай, когда происходит выход из нее — когда соединение закрыто. При этом все ошибки, связанные собственно с тем, что творилось в этом соединении, обрабатываются локально и не выпускаются наружу. L>Попытка выпустить исключение для обработки в вызываемом контексте ИМХО убивает все преимущества использования корути на корню и запутывает код.
У меня другой юзкейс. Работа с соединением происходит в асинхронной манере с помощью обычных колбеков, без всяких заморочек. IMO это лучше, если код не очень сложен. Корутины я использую для парсинга данных пришедших от сервера. Мой сервер умеет read ahead. Обычно сервер сначала должен прочитать из сокета заголовок сообщения фикс. размера, потом узнать размер тела сообщения и начать читать его. Получается две операции чтения на одно сообщения. Вместо этого мой сервер берет буфер и читает столько данных, сколько в буфер поместится, после того как данные получены, буфер содержит нецелое количество сообщений. Написать парсер данных, который будет без лишнего копирования парсить это дело и куда-нибудь отдавать довольно сложно. Тут приходят на помощь корутины. Я организовал все так, чтобы код сервера просто отдавал буфер парсеру, а код парсера просто выполнялся последовательно в корутине и читал данные из потока, как только буфер закончился, парсер делал yield и управление возвращалось серверу. Получается что данные не копируются и код парсера работает так, словно данные лежат в одном большом массиве, т.е. выглядит абсолютно линейным и простым. У меня такой сервер обрабатывает чуть больше двух миллионов сообщений в секунду на амазоновском сервере.
void ProtocolParser::worker(Caller& caller) {
// Remember caller for use in ByteStreamReader's methods
set_caller(caller);
// Buffer to read strings fromconst int buffer_len = RESPStream::STRING_LENGTH_MAX;
Byte buffer[buffer_len] = {};
int bytes_read = 0;
// Data to read
aku_ParamId id = 0;
std::string sid;
bool integer_id = false;
aku_TimeStamp ts = 0;
double value =.0;
//try {
RESPStream stream(this);
while(true) {
// read idauto next = stream.next_type();
switch(next) {
case RESPStream::INTEGER:
id = stream.read_int();
integer_id = true;
break;
case RESPStream::STRING:
bytes_read = stream.read_string(buffer, buffer_len);
sid = std::string(buffer, buffer + bytes_read);
integer_id = false;
break;
case RESPStream::BULK_STR:
// Compressed chunk of data
bytes_read = stream.read_bulkstr(buffer, buffer_len);
consumer_->add_bulk_string(buffer, bytes_read);
continue;
default:
// Bad frame
{
std::string msg;
size_t pos;
std::tie(msg, pos) = get_error_context("unexpected parameter id format");
BOOST_THROW_EXCEPTION(ProtocolParserError(msg, pos));
}
};
...
Тут все обращения к методам stream могут сделать yield и вернут управление серверу, а когда появится буфер с новыми данными — все продолжит исполняться с того же места, оч. удобно, не нужно городить сложную стейт машину для парсинга.
Собственно исключение там нужно для того, чтобы в случае ошибки отправить клиенту назад сообщение об ошибке, описывающее где именно его данные неверны, и затем закрыть сообщение. Парсер сделать это не может, значит он должен как-то передавать эту инф. на верхний уровент — TCP серверу. Таскать контекст при передаче каждого буфера и проверять его — накладно и сложно (усложняет код парсера), а вот исключения очень даже хорошо работают в этом случае.
CG>Стек трейс ты теряешь даже без исключений, мне не очень нужен контекст когда я обрабатываю исключение, но если приложение падает, каким угодно образом, мне нужен стек трейс в дампе, но вот его я как раз не получу с корутинами.
Я не смотрел пока на линукс, но для получения дампа с нормальнм стеком при взрыве в корутине в Виндовс есть работающее решение — код корутины заворачивается в __try, а __except вызывает фильтр, который и занимается созданием дампа. Получается нормальный дамп с живым стеком корутины.
CG>У меня другой юзкейс. Работа с соединением происходит в асинхронной манере с помощью обычных колбеков, без всяких заморочек.
Такой же, на самом деле. Я использую асинхронные методы asio, которые просто из колбеков передают управление в корутину. Один-в-один то, что у тебя.
GC>Вместо этого мой сервер берет буфер и читает столько данных, сколько в буфер поместится, после того как данные получены, буфер содержит нецелое количество сообщений. Написать парсер данных, который будет без лишнего копирования парсить это дело и куда-нибудь отдавать довольно сложно. Тут приходят на помощь корутины.
+
CG>Собственно исключение там нужно для того, чтобы в случае ошибки отправить клиенту назад сообщение об ошибке, описывающее где именно его данные неверны, и затем закрыть сообщение. Парсер сделать это не может, значит он должен как-то передавать эту инф. на верхний уровент — TCP серверу. Таскать контекст при передаче каждого буфера и проверять его — накладно и сложно (усложняет код парсера), а вот исключения очень даже хорошо работают в этом случае.
Тебе тут вообще эксепшен нафиг не уперся. Ну совсем:
void myCoro(Caller& caller)
{
// a lot of code hereif (error)
{
caller.setError(ErrorMessage("Parsing error at position %d", pos));
return;
}
}
дальше элементарно. Но возникает вопрос, что делать, если stream.read_string() обламывается из-за потери соединения? ИМХО, примерно то же самое, а уж вызываемый контекст пусть сам решает, что ему делать — переподключаться, ждать и т.п.
L>Тебе тут вообще эксепшен нафиг не уперся. Ну совсем:
L>
L>void myCoro(Caller& caller)
L>{
L> // a lot of code here
L> if (error)
L> {
L> caller.setError(ErrorMessage("Parsing error at position %d", pos));
L> return;
L> }
L>}
L>
Можно и так сделать, да.
L>дальше элементарно. Но возникает вопрос, что делать, если stream.read_string() обламывается из-за потери соединения? ИМХО, примерно то же самое, а уж вызываемый контекст пусть сам решает, что ему делать — переподключаться, ждать и т.п.
Он не может потерять соединение, он просто буфер парсит и все. Потерять соединение может тот, кто вызывает корутину, вот он то уже и знает как обрабатывать потерю соединения. Все вполне логично.
L>>дальше элементарно. Но возникает вопрос, что делать, если stream.read_string() обламывается из-за потери соединения? ИМХО, примерно то же самое, а уж вызываемый контекст пусть сам решает, что ему делать — переподключаться, ждать и т.п.
CG>Он не может потерять соединение, он просто буфер парсит и все. Потерять соединение может тот, кто вызывает корутину, вот он то уже и знает как обрабатывать потерю соединения. Все вполне логично.
А если вызывающий решин, что нужно начать сначала, как он корректно завершает корутину в таком случае? Просто интересно.
В моем случае только изначальное выполнялось снаружи корутины. Как только соединение появлялось, и для него появлялась работа, т.е. объект типа "TaskQueue" становился не очень пустым, соединение и очередь передавались в корутину, а дальше вся ответственность за работу с соединением была на самой корутине. Выход из корутины только в двух случаях — нечего делать и/или облом соединения, причем первый случай был сделан исключительно для того, чтобы была практическая возможность сэкономить на стеке и иметь меньше корутин, чем соединений.
CG>>Он не может потерять соединение, он просто буфер парсит и все. Потерять соединение может тот, кто вызывает корутину, вот он то уже и знает как обрабатывать потерю соединения. Все вполне логично.
L>А если вызывающий решин, что нужно начать сначала, как он корректно завершает корутину в таком случае? Просто интересно.
Он у меня не может начать сначала, при старте тисипи сессии мы создаем объект ProtocolParser — корутину и фигачим в нее пакеты с данными. Фигачиние может остановиться по инициативе извне (вызов метода close) или по инициативе изнутри (ошибка в переданных внутрь данных). Рестарта нет.
L>В моем случае только изначальное выполнялось снаружи корутины. Как только соединение появлялось, и для него появлялась работа, т.е. объект типа "TaskQueue" становился не очень пустым, соединение и очередь передавались в корутину, а дальше вся ответственность за работу с соединением была на самой корутине. Выход из корутины только в двух случаях — нечего делать и/или облом соединения, причем первый случай был сделан исключительно для того, чтобы была практическая возможность сэкономить на стеке и иметь меньше корутин, чем соединений.
У меня работа с сокетом происходит в обычном режиме, с помощью обычных колбэков, в корутину же передаются только буферы с данными, где они парсятся. Я решил всегда создавать новую крутину на соединение ибо соединения планируются долгоживущие. Посмотреть можно тут — *.h и *.cpp, там суть в том, что сервер дергает метод push_next, в котором буфер добавляется в очередь и делается yield в корутину. Когда данные зананчиваются, корутина делает yield обратно и сервер продолжает работать. Пока все это происходит, уже может прийти следующая порция данных и тут же вызовется следующий коллбек, который снова дернет push_next и так далее.
Здравствуйте, chaotic-good, Вы писали:
L>>А если вызывающий решин, что нужно начать сначала, как он корректно завершает корутину в таком случае? Просто интересно.
CG>Он у меня не может начать сначала, при старте тисипи сессии мы создаем объект ProtocolParser — корутину и фигачим в нее пакеты с данными. Фигачиние может остановиться по инициативе извне (вызов метода close) или по инициативе изнутри (ошибка в переданных внутрь данных). Рестарта нет.
У меня вопрос скорее в том, чтобы корректно завершить корутину, т.е. дать ей шанс размотать стек. А то был у нас прецендент...
Впрочем, вижу, что при закрытии стрима в случае ожидания на get() будет выброшено исключение.
CG>У меня работа с сокетом происходит в обычном режиме, с помощью обычных колбэков, в корутину же передаются только буферы с данными, где они парсятся. Я решил всегда создавать новую крутину на соединение ибо соединения планируются долгоживущие. Посмотреть можно тут — *.h и *.cpp, там суть в том, что сервер дергает метод push_next, в котором буфер добавляется в очередь и делается yield в корутину. Когда данные зананчиваются, корутина делает yield обратно и сервер продолжает работать. Пока все это происходит, уже может прийти следующая порция данных и тут же вызовется следующий коллбек, который снова дернет push_next и так далее.
А, ну это стандартный подход, на самом деле. Реализации бывают разные только.
CG>У меня работа с сокетом происходит в обычном режиме, с помощью обычных колбэков, в корутину же передаются только буферы с данными, где они парсятся. Я решил всегда создавать новую крутину на соединение ибо соединения планируются долгоживущие. Посмотреть можно тут — *.h и *.cpp, там суть в том, что сервер дергает метод push_next, в котором буфер добавляется в очередь и делается yield в корутину. Когда данные зананчиваются, корутина делает yield обратно и сервер продолжает работать. Пока все это происходит, уже может прийти следующая порция данных и тут же вызовется следующий коллбек, который снова дернет push_next и так далее.
Прошу прощения что влезаю в дискуссию. Т.к. сам недавно начал изучать корутины и их применение, тема стала интересна.
Но это офтопик, просто интересуюсь почему сделали так:
case RESPStream::STRING:
bytes_read = stream.read_string(buffer, buffer_len);
value = strtod(buffer, nullptr);
memset(buffer, 0, bytes_read);
зачем тут нужен memset() ?
Насколько я понимаю stream.read_string() (который RESPStream) читает строку из потока до '\0' а значит наверно проще будет дополнять buffer[bytes_read+1] = '\0'
А что бы гарантировать что мы не вылезем за пределы buffer можно алоцировать его на байт больше инициализировав последний '\0' значением, что наверно проще чем занулять буфер.
Хотя тут же в коде нет проверки на то что что bytes_read != 0 а в резултатет можно получить в value ложный 0.
спасибо за живой код, самому уже хочется пописать что-то с корутинами.
Здравствуйте, jazzer, Вы писали:
J>Здравствуйте, chaotic-good, Вы писали:
A>>>SSCCE или GTFO.
CG>>это невозможно по ряду причин, воспроизводится не везде, требует сложной сборки двух бинарников, в общем, я к опыту взываю, может кто просто сталкивался с подобным уже
J>пальцем в небо — исключение пересекает границу so? У меня такая радость с gcc безо всяких корутин — исключение из so в главной проге не всегда ловится (с gcc3 проблем не было, началось с gcc4).
Здравствуйте, __Nicolay, Вы писали:
J>>пальцем в небо — исключение пересекает границу so? У меня такая радость с gcc безо всяких корутин — исключение из so в главной проге не всегда ловится (с gcc3 проблем не было, началось с gcc4).
__N>О! У нас та же проблема. Как боретесь?
Да никак. Запускаем еще раз под gdb и смотрим, где отвалилось. Все равно это редко и случается обычно только на этапе инициализации, когда конфиг кривой или файл не нашелся или еще чего-нибудь... в остальных случаях все летает внутри DLL и успешно ловится.