Обновить
24
Виктор Поморцев@SpiderEkb

Консультант направления по разработке

0,4
Рейтинг
37
Подписчики
Отправить сообщение

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

Я связан контрактом. В БД все суммы хранятся в формате decimal(23,0) которого хватает в 99.99% случаев (за исключением когда на тестовом сервере накидали каких-то совсем нереальных данных). И я не могу вернуть какой-то иной тип.
Я просто показал что вместо перехвата исключения

monitor;
  targetRoubles = sourseCurrensy * cource;
on-excp 'MCH1210';
  targetRoubles = %hival(targetRoubles);
  error = ...
endmon;

С точки зрения производительности рациональнее использовать дополнительную проверку.

Есть много других примеров, просто там все более сложно и долго расписывать.

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

Именно об этом и говорю. Исключение - это непредусмотренная и необработанная ошибка.

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

Если бы все было так просто. Ошибки в данных не ловятся типами.

Простейший пример - у вас есть сумма в валюте счета. Все суммы в коммерческих расчетах имеют тип с фиксированной точкой и хранятся в миноритарных единицах. Пусть это будет decimal(23, 0) (23 знака, 0 после запятой).

А теперь вам надо получить эквивалент суммы в валюте счета в рублях. И положить ее в переменную того же типа - decimal(23, 0)

Т.е. вам надо сконвертировать сумму в валюте счета в рубли по текущему курсу и проверить не превышает ли она заданное значение. Берем сумму в валюте счета, берем код валюты, берем из справочника курс, перемножаем и... Получаем системное исключение - "MCH1210 - RECEIVER VALUE TOO SMALL TO HOLD RESULT". Ну система так работает - она для коммерческих вычислений, тут переполнение является ошибкой.

Варианта два - оставить все как есть, ловить исключение и обрабатывать. Но это нехорошо с точки зрения производительности. Значит надо использовать временну. переменную с максимальным размером - decimal(63, 0). Конвертировать в нее, потом проверять, поместится ли она в целевую и если нет - возвращать уже структурированную ошибку. Примерно так:

dcl-s sourseCurrensy  packed(23: 0);
dcl-s targetRoubles   packed(23: 0) inz(*hival);
dcl-s cource          packed(23: 0);
dcl-s intermediate    packed(63: 0);

intermediate = sourseCurrensy * cource;

if intermediate > targetRoubles;
  error = ...
else;
  targetRoubles = intermediate;
endif;

targetRoubles инициализируется как *hival - максимально возможное значение для данного типа, в данном случае 23 девятки.

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

Если возможны как положительные, так и отрицательные значения

if intermediate in %range(%loval(target): %hival(target));
  target = intermediate;
else;
  error = ...
endif;

%loval/%hival тут функции, возвращающие минимально/максимально возможные значения переменной.

В любом случае, такая проверка по потреблению ресурсов процессора обходится значительно дешевле чем исключение.

Это очень простой пример. Есть более сложные, когда данные, сами по себе валидны, но не проходят в разрезе бизнес-логики отдельно взятого процесса.

Я смотрю сквозь призму опыта. Мне не кажется хорошим стилем просто крашить задачу без подробного объяснения причин в стиле "Что-то пошло не так...".

Для меня обработка ошибок всегда была более сложным местом, чем реализация логики.

В том и дело что задания у нас крашатся в самых исключительных случаях.

А 99% случаев ошибки перехватываются и обрабатываются. Т если есть возможность продолжать работу - работа продолжется.

Условно говоря, если где-то случилось деление на 0 потому что в данных попался 0 там, где его быть не должно - ошибка будет зафиксирована подробно - "там-то и там-то некорректные данные - 0"

Аналогично - переполнение (у нас переполнение вызывает системное исключение).

А вот когда начинает систематически крашится какой-то конкретный модуль - тут уже сопровождение обычно откатывает последний патч (если началось после установки нового патча - с патчем всегда идет "план отката").

есть и задачи, которыми занимаетесь вы (и для которых озвучиваемые вами требования актуальны), и еще туева хуча задач, о которых вы никогда и не слышали

Уверен что так и есть. Хотя за... 35 лет в разработке много чем занимался в разных областях и много чего повидал.

Ну есть разница по объему и времени тестировании между регресс-тестом изменений и полным ретестом.

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

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

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

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

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

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

Как пример - обработка 25 000 000 записей в рамках заданного временного окна и условиях ограничения ресурсов. В 10-ти записях (условно, может быть 100, может быть 1000) возникает какая-то ошибка, связанная со специфическим набором данных в конкретной записи (скорее всего, какое-то количество таких ошибок будет всегда). Такие ошибки необходимо фиксировать для последующего ручного разбора. Но все остальные записи надо продолжать обрабатывать. Вот тут исключения категорически непригодны т.к. снижают производительность.

Приложение не имеет права "вот так просто завершиться". Любая ошибка должна быть зафиксирована и обработана. Для этого вы всегда должны четко знать где она случилась и почему - ошибка в программе, ошибка в логике, ошибка в данных...

Если эти 1-2 строчки будут работать быстрее - почему нет?

А если это потянет 5 уровней абстракций и кучу выделений-освобождений памяти в результате которых упадет производительность - точно нет.

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

А так и делаем. Структурированная ошибка называется.

Беда в том, что в этом самом hot path практически все...

Если он хорошо читается и удовлетворяет требованием производительности - почему нет?

Основная проблема в том, что порой чтобы понять что именно делает код из (условно) 100 строк вам надо продраться через 1000 (опять условно) строк другого кода, содержащего десяток слоев абстракций. И не дай бог, если проблема (дефект бизнес-логики, дефект производительности...) окажется где-то там...

Увы, но использование исключений катастрофически отрицательно влияет на производительность... Вплоть до того, что на нагрузочном тестировании вам вынесут приговор - "20% ресурсов процессора уходит на работу с исключениями - переделать".

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

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

Ох как согласен...

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

И Вы не зря упомянули PL/I - он рухнул именно по причине непомерно возросшей сложности. С++ уверенно идет туда же и держится на плаву исключительно за счет накопленной огромной кодовой базы.

Сам я начал писать на С в... 89-м, по-моему, году. Потом был "С с классами" (который очень нравился), потом С++, который нравился чем дальше тем меньше. И так до 17-го года, когда волею судеб занесло на достаточно экзотическую у нас платформу со своим стеком (да, С/С++ тут тоже есть, но это не основной язык) в разработку большой системы с высокими требованиями к надежности и производительности (АБС крупного банка).

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

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

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

Ну тут дело личных предпочтений. Суть одна.

Громоздко когда после каждого блока будете if error... проверять.

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

Такие блоки по уму надо вообще в отдельные функции выводить. Чтобы не получить одну функцию на 10 экранов.

И тогда нормально будет если каждая такая функция возвращает успешно/не успешно + ошибку в параметре. Тогда вообще просто:

if (func1(...) && func2(...) && ... && funcN(...)) {
  // успешное выполнение
} else {
  // была ошибка - разбираемся где
}

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

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

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

Тут влияет специфика платформы - мы работаем на IBM i (AS/400), тут БД (DB2) является частью ОС, интегрирована в нее на низком уровне (включая SQL движок) и посему очень быстрая. Особенности файловой системы, что тут нет файлов как таковых. Все есть объект. Есть объект типа PF - физический файл. Это таблица. Есть LF - логический файл, индекс (несколько шире на само деле, но ближе всего индекс), есть DSPF - дисплейный файл, описание экранной формы, есть PRTF - принтерный файл, описание формы для вывода на печать (в спул печати). А вот просто текстового файла тут нет (как хранятся исходники лучше не спрашивать - начну рассказывать во всех подробностях :-)

Параллельно нативной ФС есть еще "интегрированная файловая система (IFS). Это уже UNIX-like ФС.

Т.е. варианты вывода - в таблицу БД, на IFS или в спул печати. Так вот в БД не только самый удобный для последующей работы - интерактивный скуль тут есть как в терминале, так и отдельным приложением, не требующим терминала (оно еще и умеет планы запросов показывать и диагностику всякую), но и самый быстрый и экономичный по ресурсам.

Все остальное - да, выросло в процессе работы. Система очень большая, команд много. У каждой команды в принципе своя подходы. Принципы общие, но в деталях могут быть различия. Одно время (где-то 18-19-й годы) обсуждалась централизованная система логирования, но потом пришли к тому, что будет слишком громоздко и сложно учесть во всех деталях требования всех команд. А поскольку каждая команда отвечает за свой участок (скажем, мы, комплаенс, никогда не полезем в зону ответственности той же системы расчетов или тарифного модуля), то у каждой команды свои логи. Иногда (по необходимости), их может быть несколько - у нас есть общий и еще пара под конкретные линейки, там, где требуется какая-то специфическая информация помимо той что описал выше).

В целом хороший пример того, что в проде важнее управляемость и структура.

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

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

Но здесь идет речь исключительно о продуктовых логах. Т.е. тех ошибках, которые перехватываются и обрабатываются. К сожалению, бывают и падения - необработанные исключения. Это уже отражается в системных логах - у каждого задания всегда есть свой joblog, который ведется системой (мы, конечно, можем туда сообщения кидать, но стараемся этого не делать - они и так достаточно объемные). Пока задание активно, его можно смотреть в терминале системной командой или скулем с использованием системной UDTF. По завершению задания joblog преобразуется в спульник (spool file) и уходит в спул печати профайла из под которого это задание было запущено. Дальше его уже можно с сервера на комп вытащить в виде txt или pdf. Там все достаточно подробно, примерно вот так:

MCH6802    Escape                  40   28.03.26  19:56:13.901238  QLEAWI       QSYS        *STMT    QLEAWI      QSYS        *STMT
                                     From module . . . . . . . . :   QLEDTAWI
                                     From procedure  . . . . . . :   CEEUTCO
                                     Statement . . . . . . . . . :   2
                                     To module . . . . . . . . . :   QLEDTAWI
                                     To procedure  . . . . . . . :   CEEUTCO
                                     Statement . . . . . . . . . :   2
                                     Message . . . . :   Literal values cannot be changed.
                                     Cause . . . . . :   An attempt was made to write over a literal (constant)
                                       value. A common cause of this exception condition is supplying the address
                                       of a literal as an argument when the called code is expecting to return
                                       information at that address. Note that all program calls pass arguments by
                                       address. The object that contains the literal value is QRNXUTIL.

И сопровождение говорит "там у вас упало", то сразу "давайте джоблоги и если есть дампы".

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

Все верно в общем и целом. Пришли примерно к тому же (плюс-минус, учитывая нашу специфику - у нас вся работа с БД, соответственно, логи тоже пишутся в таблицы БД + есть настроечные таблицы для каждой системы логирования)

Мы пришли к такой структуре. В настройках:

  • Узел логирования (близко к тому, что у вас называется "подсистема")

  • Текущий уровень логирования (остановились на трех уровнях - E - вывод только ошибок; B - вывод ошибок и бизнес сообщений и T - ошибки, бизнес сообщения и трейсы)

  • Статус активности узла

  • Глубина хранения лога для каждого уровня логирования (например, D007 для E; D005 для B и D002 для T) - поскольку все в таблицах БД, сопровождение в начале каждого дня запускает скрипт прочистки лога где удаляется все, выходящее за глубину хранения.

В самом логе:

  • Дата

  • Таймштамп

  • Узел логирования

  • Полное имя задания (у нас все работает в изолированных заданиях - job, потоки внутри задания используются мало)

  • Уровень логирования

  • Ключ сообщения (используется для дополнительной фильтрации, может использоваться, например, для связки сообщения об ошибки с конкретной записью в БД при обработке которой эта ошибка произошла)

  • Данные сообщения

  • Точка вызова (позволяет привязать сообщение к конкретной точке кода где оно случилось)

Кроме собственно функций записи в лог есть функция AllowLog где указывается узел логирования и уровень логирования. Сделана она для того, чтобы не тратить время на формирование сообщения лога в тех случаях, когда оно до лога не дойдет (узел логирования не активен или для него установлен уровень E, а сообщение имеет уровень B или T). Это уже было добавлено позднее по результатам нагрузочных тестов.

Поскольку логи в БД, то работа с ними потом обычным скулем - просто, быстро и никаких проблем с фильтрацией по любому параметру (сочетанию параметров).

1
23 ...

Информация

В рейтинге
2 750-й
Откуда
Екатеринбург, Свердловская обл., Россия
Работает в
Дата рождения
Зарегистрирован
Активность