All streams
Search
Write a publication
Pull to refresh
23
0.1
Виктор Поморцев @SpiderEkb

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

Send message

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

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

Дефект промсреды имеет максимальный приоритет. Т.е. бросить все и исправлять.

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

А 99% ошибок - ошибки связанные с бизнес-логикой. И там исключения не нужны. Все идет через структуриованные ошибки. Даже есть механизм (сервис) стека ошибок который работает в рамках задания.

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

MCH1202    Аварийное               40   14.07.25  10:52:51.511786  AGT01R       ALIBOP1     *STMT    AGT01R      ALIBOP1     *STMT  
                                     От модуля . . . . . . . . . :   AGT01R                                                         
                                     От процедуры. . . . . . . . :   AGT01R                                                         
                                     Оператор  . . . . . . . . . :   64300                                                          
                                     Для модуля. . . . . . . . . :   AGT01R                                                         
                                     Для процедуры . . . . . . . :   AGT01R                                                         
                                     Оператор  . . . . . . . . . :   64300                                                          
                                     Сообщение . . . :   Decimal data error.                                                        
                                     Cause . . . . . :   The sign or the digit codes of the packed or the zoned                     
                                       decimal operand is in error.  Valid signs are hex A-F, valid digit range is                  
                                       hex 0-9. 

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

Теория утверждает, что результаты практики должны соответствовать теории, но на практике это не так.

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

То, что рушит бизнес-процесс, должно исправляться в кратчайшие сроки. У нас так.

Потому что конечная цель - бизнес-логика, а не отлов ошибок.

Такими темпами мы сейчас перепишем вашу систему на .НЕТ и отпрофилируем заодно.

Ваш нет не поддерживает SQL непосредственно в коде. Ваш нет не поддерживает типы данных, которые есть в БД (всякие фиксированные точки с арифметикой, в т.ч. и арифметикой с окрглением, дату-время со всеми форматами и арифметикой и т.п.) Вам на все надо внешние зависимости и объекты создавать. А у нас это все "из коробки" прямо в языке. Плюс возможность прямого доступа к БД через индексы (чтобы получить одну или десяток записей по известному значению ключа, скуль - мегаизбыточно).

А работа с БД - 99.9% нашей деятельности. И БД у нас - часть ОС. Т.е. программно-аппаратная реализация.

Так что, нет. Спасибо, но не надо :-)

Валидация тяжелая, IsValid не возвращает деталей - ок

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

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

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

Во-вторых, ошибка может возникнуть в процессе обработки данных. Например, пытаетесь что-то записать в БД, получаете ошибку по дублированию ключа. Ошибка? Да. Но ее обработка сильно зависит от контекста.

Пример. Есть обработка... Ну некоторых данных, так скажем. Там в течении дня где-то под сотню миллионов проходит.

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

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

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

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

Таких ошибок вообще не должно быть. У нас это называется "дефект промсреды" который подлежит исправлению.

Обычно это что-то типа Decimal data error (когда не проверили что пришло на вход) или Variable too small to hold result - переполнение (да, у нас переполнение вызывает системное исключение если вы попытаетесь в двухбайтовое целое записать 4 байта - получите исключение). Такие вещи надо проверять и формировать структурированную ошибку что результат фактически неопределен. А дальше уже опять по логике задачи действовать.

А вот если Process или Postpone не справились - то они выкидывают исключение, которое улетает выше - и там уже, возможно, мы решаем что с этим делать. Потому, что это явно случай, к которому мы не готовы, не важно по какой причине, но что делать мы на этом уровне не знаем.

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

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

if (IsValid(item))

не дает никакой информации о том, а что именно там не прошло валидацию.

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

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

Вот файл сообщений

Более полная информация по конкретному сообщению

Структурированная ошибка содержит ИД сообщения + блок данных (параметров) которые будут подставляться вместо &1, &2, &3 и т.п.

В принципе, в процессе обработки обычно бывает достаточно ИД сообщения (тип ошибки). А в лог пойдет полная текстовая расшифровка со всеми подставленными параметрами.

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

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

Интерфейс у нее такой

      // --------------------------------------------------
      // Prototype for main procedure
      // --------------------------------------------------
      DCL-PROC INNSMEVRQ;
        DCL-PI *N;
          Dte       zoned(7: 0);
          Error     char(37)  options(*nopass);
        END-PI ;

Dte тут есть некоторый параметр, Error - та самая структурированная ошибка (у нас структура тождественна строке - 37 символов это 7 символов на ИД + 3 параметра по 10 символов).

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

А далее

      // Этапы вполнения
      dcl-enum enStages qualified;
        stageAccounts   1;
        stageNames      2;
        stageUnconfGRF  3;
        stageChngUID    4;
        stageChngCBH    5;
        stageCLJ        6;
        stageCrdHldr    7;
        stageSend       8;
      end-enum;

        dcl-s  stage           int(5);
        dcl-ds dsError         likeds(t_dsError37);

        // Заполняем список и вызываем процедуру отсылки запроса в СМЭВ
        clear dsError;

        for-each stage in enStages;
          select stage;
            when-is enStages.stageAccounts ; // Отбор по счетам
              procAccounts(procDte: dsError);

            when-is enStages.stageNames    ; // Отбор по изменению Имени
              procNames(procDte: dsError);

            when-is enStages.stageUnconfGRF; // Отбор по неподтвержденному ИНН
              procUnconfCRF(finnstFrom: finnstTo);

            when-is enStages.stageChngUID  ; // Отбор по изменению идентификации
              procUID(finnstFrom: finnstTo);

            when-is enStages.stageChngCBH  ; // Отбор по изменению статуса самозанятого
              procCBH(procDte);

            when-is enStages.stageCLJ      ; // Отбор из витрины ЖМ
              procCLJ(procDte);

            when-is enStages.stageCrdHldr  ; // Отбор держателей карт из МПК
              procCardHolders(procDte);

            when-is enStages.stageSend     ; // Отсылка запросов
              sendRequests(dsError);
          endsl;

          // Если на каком-то из этапов вернулась ошибка - завершаем работу
          if dsError <> *blanks;
            leave;
          endif;
        endfor;

        if %passed(Error);
          Error = dsError;
        endif;

        return;

Т.е. в цикле вызываем все этапы (в том порядке, как они определены в enStages, если на каком-то этапе произошла ошибка - выходим из цикла. И, если "сверху" попросили вернуть ошибку (вызвали с параметром Error) - пробрасываем ее наверх.

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

Если где-то внутри ошибка таки возникла, то она будет там же залогирована и возвращена наверх.

Например, в procAccounts и procNames источником ошибки может быть SQL запрос. Тогда оттуда будет вызвана процедура SQLError

      DCL-PROC SQLError ;
        DCL-PI *N;
          procName  char(64) const;
          dsError   likeds(t_dsError37);
        END-PI ;

Внутри которой

        exec sql GET DIAGNOSTICS CONDITION 1
                  :SQLID    = DB2_MESSAGE_ID,
                  :SQLSTATE = RETURNED_SQLSTATE,
                  :errText  = MESSAGE_TEXT;

        dsError.errCode  = 'FIN0019';
        dsError.errParm1 = sqlid;
        dsError.errParm2 = sqlstate;
        dsError.errParm2 = procName;

        AGFRLogText(C_INNSMEV_NODE: '': 'E': 'SQL Error: ' + %subst(errText: 1: 253): *omit: *omit: procName);

AGFRLogText - запись ошибки в лог. Туда пойдет тот текст, что вернул SQL движок в поле MESSAGE_TEXT.

А наверх уйдет структурированная ошибка с кодом FIN0019 (это наш код, под конкретный модуль) которая описана как

        messageID = "FIN0019"        // message ID. Set it if not match with OBJECTNAME
		messageText = "Ошибка &1 со статусом &2 выполнения SQL запроса в процедуре &3"
		messageSeverity = "20"     
		messageFile = "KSMMSGF"    
		messageFileLib = "%KLIB%"  
		invertMessage = false 
		replaceMessage = true 
		lengths = [10,10,10]

И в качестве параметров в нее будут подставлены SQL код (DB2_MESSAGE_ID), SQL статус (RETURNED_SQLSTATE) и имя процедуры где она возникла.

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

Вот в том и дело что исключение - это слишком дорого. По крайней мере для нас.

Но у нас стек специфический очень. Суть в том, что все построено на "сообщениях". Сообщение - это идентификатор (7 символов), текст (с возможностью подстановки параметров) и уровень серьезности (информация, предупреждение и т.п.). Сообщения можно использовать как имеющиеся, так и добавлять свои.

Данные по сообщениям хранятся в т.н. файлах сообщений (что-то типа таблицы БД).

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

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

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

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

Условно говоря:

dcl-proc procA;
  dcl-pi *n;
    dsError likeds(t_dsError);
  end-pi;

  clear dsError;
  procB(dsError);

  if dsError <> *blanks;
    return;
  endif;

  ...

  return;
end-proc;

Примерно так. Это намного дешевле чем делать

snd-msg *escape %msg(dsError.code: msgFile: dsError.data);

в procB (snd-msg *escape в данном случае есть аналог throw)

и

monitor;
  procB();
on-error;
  return;
end-mon;

в procA или где-то выше по стеку (monitor - аналог try, on-error или on-excp с нужным кодом ошибки - аналог catch).

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

А snd-msg вообще очень гибкая штука

В моём понимании (и практике) ровно наоборот - условные "99% ошибок" внутри процессинга - это прерывающие исключения.

Какого рода ошибки?

Если речь о чем-то типа деления на ноль, то они вызывают системное исключение которое перехватывается дефолтным обработчиком с формированием дампа и автоматически (системой же) фиксируются в joblog задания. Это уже "дефект промсреды" - он передается на исправление.

Здесь же речь о другом изначально.

Всё, что не прерывает - это нормальный случай, он выбирается по условиям/анализу/валидации параметров-данных

Вот. А дальше? Данные невалидны. Что дальше? Вернуть какой-то result или бросить исключение через throw? Вот о чем разговор.

А что вы строите на исключениях? 99% "ошибок" - это логика, требующая обработки. Причем, в разных сценариях использования логика обработки одной и той же ошибки может быть существенно разной. В каком-то случае это прерывающая ошибка, в каком-то просто логируемая без прерывания общего потока обработки, в каком-то ее вообще можно игнорировать как несущественную.

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

Как вы себе представляете работу try/catch без throw где-то внутри?

throw дорогой, но - можно пример сценария, при котором throw начинает влиять на производительность нормального/expected случая, а значит и среднюю производительность?

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

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

Тут не только общая производительность сервера может деградировать, тут еще нагрузка на сервер будет расти.

Напротив, Result "запихивает поглубже": в исключении, как правило, есть информация о стеке, а в результате, как правило, нет.

Это уже фактически проработанная ошибка. Там не нужна информация о стеке.

Существенной разницы между try/catch и if/else при этом не возникает.

Ровно до тех пор, пока не прогонишь реальную задачу с большой плотностью вызовов через профайлер

Но ведь именно для устранения бойлерплейта, связанного с обслуживанием Result, исключения и используют.

Не для устранения, а для "запихивания его поглубже под..."

Другое дело, что неопытные программисты часто не могут понять, где логика, а где ошибка

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

Разницы нет до тех пор пока не начнут строго спрашивать за производительность. А вот когда начнут, то удобнее сразу написать как эффективнее работает (а не как писать удобнее) чтобы не переделывать все после провала на нагрузочном тестировании - это точно неудобно с точки зрения разработки.

Ну и избегать большой вложенности вызовов (и большой глубины стека) - это тоже хороший стиль.

goto и вызов функции принципиально разные вещи.

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

goto же - это как прыжок в другую часть тропинки. Причем, не обязательно вперед. Может быть и назад. Тут вся линейность выполнения меняется.

А как эта сама автоматика узнает когда нужен rollback, а когда commit?

А если вы обрабатываете некоторое сообщение из очереди и обязаны в очередь же отправить какой-то ответ в любом случае?

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

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

Речь может идти не о тех ресурсах, которые могут быть очищены автоматически, но о какой-то обязательной логике типа commit/rollback или отсылке куда-то какого-то результата в зависимости от того, что успели или не успели сделать в процессе выполнения.

Это делается ради единой точки выхода. Когда требуется подчистка выделенных локально ресурсов.

Есть. Но нужен сам эмулятор для этого.

Минимальная совместимость поддерживается обычным телнетом. По крайней мере простенькие экранные формы.

А вот сложные формы со всеми возможностями дисплейного DDS - тут уже сложнее. Там же не "картинка" идет, а структуры с управляющими кодами. DDS это описание типа такого:

      *
     A          R MSGSFL                    TEXT('*** Message subfile ***')
     A                                      SFL SFLMSGRCD(24)
     A            FLDKEY                    SFLMSGKEY
     A            FLDPGM                    SFLPGMQ
      *
      *-------------------------------------------------------------------------
      *
     A          R OPTION                    SFLCTL(MSGSFL)
     A                                      SFLSIZ(0020)
     A                                      SFLPAG(0001)
     A                                      TEXT('*** Message control ***')
     A                                      BLINK
     A                                      OVERLAY
     A                                      PUTOVR
     A  82                                  SFLDSP
     A  83                                  SFLDSPCTL
     A  82                                  SFLINZ
     A  82                                  SFLEND
     A N82                                  ERASE(MSGSFL)
     A            FLDPGM                    SFLPGMQ
     A            ZLCTXT    R        O 23  2REFFLD(CMD)
     A                                      OVRDTA
     A                                      COLOR(BLU)

После компиляции структура его во многом схожа со структурой обычной таблицы БД - то же набор полей с атрибутами (на AS/400 что описание таблицы и ее структуру можно посмотреть командами DSPFD/DSPFFD, что описание и структуру дисплейных или принтерных файлов - все они описываются однотипно, с помощью DDS)

Ну вот у нас используются Indeed Key на телефоне (до этого - RSA Software token). Т.е. первое что вводится - 6 цифр, которые генерируются приложением на телефоне и меняются каждый 30 секунд. Приложение (точнее, сгенерированный им после установки идентификатор) зарегистрировано на внутреннем сервере идентификации и связано с конкретным профайлом пользователя. Идентификатор привязан к конкретному телефону (т.е. поменял телефон, установил на новый приложение - нужно регистроваться заново).

Т.е. первый этап - логин + код, второй этап - логин + пароль.

Т.е. чтобы воспользоваться профайлом конкретного человека вам еще в его телефон залезть надо.

Ну, насколько знаю, там блочный протокол обмена.

И сколько видел этих самописных терминалов - они все в чем-то да ограничены. В плане поддержки всех возможностей DDS для дисплейных файлов.

Сейчас этих эмуляторов...

Для IBM i сама IBM предлагает бесплатный пакет IBM i Access Client Solutions Среди многих полезных компонент там есть и эмулятор терминала IBM5250.

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

Information

Rating
3,002-nd
Location
Екатеринбург, Свердловская обл., Россия
Works in
Date of birth
Registered
Activity