Продолжение цикла статей "что такое RPG и как с ним работать.
Предыдущие статьи
Современный RPG — что может и зачем нужен
Способы работы с БД DB2 в языке RPG на платформе IBM i
Как и прошлые статьи, эта носит исключительно обзорный характер, не претендуя на полноту изложения всех тонкостей работы с ошибками и исключениями на платформе IBM i в общем и в RPG в частности.
Преамбула
В настоящее время RPG существует (и активно используется) только на платформе IBM i. Эта платформа (как и сам RPG) позиционируется как платформа для коммерческих серверов. Т.е. для разного рода финансовых (главным образом) расчетов. Ошибки в этой области стоят очень дорого в прямом денежном выражении. Поэтому к обработке ошибок здесь принято относиться с с особым вниманием и тщательностью. И сама платформа и язык тому способствуют. Как пример - обычное переполнение (в отличии от других языков) в RPG вызывает исключение "Receiver value too small to hold result" аналогично исключению, возникающему при попытке деления на ноль. Попробуете присвоить значение, например, 60 000 переменной, объявленной как 16-битное знаковое целое - получите системную ошибку (исключение).
Любая ошибка здесь должна быть как минимум подробно зафиксирована, проанализирована и по возможности обработана. Будь то системная ошибка или ошибка бизнес-логики. И сама платформа, и язык предоставляют разнообразные возможности для работы с ошибками.
Сообщения, файлы сообщений
Основой для генерации ошибок и исключений в IBM i являются т.н. "сообщения".
Сообщение состоит из
Код (идентификатор) сообщения - 7 символов, традиционно первые три символа обозначают "мнемонику" (например, сообщения от SQL движка начинаются с SQL..., системные - MCH... или CPF... и т.п.), следующие 4 - номер внутри мнемоники.
Текст сообщения. Может содержать места для подстановки параметров (&1, &2 и т.д.) передаваемых как блок дополнительных данных
Уровень серьезности сообщения - 00 - информационное, 10 - предупреждение, 20 и выше - ошибка.
Количество и формат параметров
Например, сообщение с кодом KSM2010
ИД сообщения Серьезность Текст сообщения KSM2010 20 &1&2&3 does not exist on the database
Имеет три параметра
Поле Тип данн. Длина &1 *CHAR 10 &2 *CHAR 10 &3 *CHAR 10
Сообщения хранятся в т.н. message files - объект типа *MSGF. Можно создавать свои файлы сообщений, добавлять туда свои типы сообщений, все это реализовано на уровне системных команд.
В нашей практике принято для каждой продуктовой линейки задавать свою "мнемонику" (3-символьный код) и использовать ее к качестве первых трех символов кода сообщения.
Для получения полного текста сообщения (с подставленными параметрами) есть системное API QMHRTVM (Retrieve Message) куда передается код сообщения, дополнительные данные (используемые для подстановки) и на выходе получим полный текст сообщения.
Например, передав туда 'KSM2010' и 'Client XXXXXX' получим текст 'Client XXXXXX does not exist on the database'
Структурированная ошибка
Данные для получения текста сообщения могут возвращаться в виде т.н. "структурированной ошибки". Системные API обычно используют следующую структуру (минимальный вариант)
typedef struct Qus_EC { int Bytes_Provided; int Bytes_Available; char Exception_Id[7]; char Reserved; char Exception_Data[]; /* Varying length */ } Qus_EC_t;
Где
Exception_Id - код сообщения, например, 'KSM2010'.
Exception_Data - блок дополнительных данных (переменной длины), например, 'Client XXXXXX'
Bytes_Provided - общий размер памяти, выделенный под эту структуру включая блок дополнительных данных, заполняется перед вызовом функции куда передается.
Bytes_Available - размер реально заполненных данных - определяется размером блока реально заполненных дополнительных данных, заполняется внутри вызываемой функции при формировании ошибки.
В RPG мы используем еще более простую версию:
dcl-ds t_dsError37 qualified template; errCode char(7) inz; errParm1 char(10) inz; errParm2 char(10) inz; errParm3 char(10) inz; errParmAll char(30) samepos(errParm1); errParm12 char(20) samepos(errParm1); errParm23 char(20) samepos(errParm2); errStr char(37) samepos(errCode); end-ds;
Код ошибки + 3 параметра по 10 символов каждый (соответственно, все наши сообщения, которые мы сами создаем, имеют три 10-символьных параметра - для наших целей этого достаточно), плюс возможные их комбинации для случаев когда в тексте ошибки используются блоки типа &1&2&3, &1&2, &2&3. В приведенном выше примере:
dcl-ds dsError likeds(t_dsError37); ... dsError.errCode = 'KSM2010'; dsError.errParmAll = 'Client XXXXXX';
Очереди сообщений
В IBM i есть такой объект как "очередь сообщений" - *MSGQ. Мы можем создать свою очередь и посылать туда произвольные текстовые сообщения с тем, чтобы кто-то другой мог их оттуда извлечь. Для посылки сообщения в произвольную очередь есть системное API QMHSNDM (Send Nonprogram Message), для чтения сообщения из произвольной очереди - API QMHRCVM (Receive Nonprogram Message). Этот механизм бывает удобен для обмен сообщения между программами, работающими в разных заданиях или для мониторинга работы программы в реальном времени - в эмуляторе терминала можно подключиться к очереди сообщений и просматривать ее содержимое.
В RPG для посылки сообщения в произвольную очередь можно использовать оператор dsply с указанием имени очереди
dsply 'My message' 'MYMSGQ';
Особым случаем очереди сообщений является "очередь сообщений программы" - *PGMQ которая является частью стека вызовов - каждому уровню вызова (программе или процедуре в стеке) соответствует своя очередь сообщений. Это позволяет отправлять сообщения конкретной программе (например, предыдущей в стеке вызовов). Для работы с этими очередями также существуют системные API QMHSNDPM (Send Program Message) и QMHRCVPM (Receive Program Message)
Исключения
А что с исключениями? Тут все просто. IBM i предоставляет механизм системных, не зависящих от языка, исключений базирующихся на тех же самых сообщениях посылаемых в *PGMQ. Более того, поскольку на каждом уровне стека есть своя очередь сообщений, можно гибко управлять куда именно посылать исключение - самому себе (тот же уровень стека), вызывающей программе или выше по стеку с указанием от какого уровня и на сколько уровней вверх.
При посылке сообщения в *PGMQ необходимо указывать тип сообщения - информационное, диагностическое, прерывающее и т.п. Исключением (в привычном понимании) будет сообщение, посланное с типом "прерывающее" - *ESCAPE
Такой механизм позволяет не привязываться к конкретным языкам и их исключениям (которых может не быть или которые могут быть не совместимыми друг с другом), но использовать единый механизм, ограниченный только рамками задания (job) а не одной конкретной программы.
Более того, все сообщения (вне зависимости от типа), посланные в *PGMQ, автоматически фиксируются в логе задания (joblog) в котором работает программа, пославшая сообщение. Выглядит это примерно так:
CPF2105 Аварийное 40 24.04.25 10:23:58.882687 QLIDLOBJ QSYS 072F UTM10C1 KAPBASELIB *STMT Для модуля. . . . . . . . . : UTM10C1 Для процедуры . . . . . . . : UTM10C1 Оператор . . . . . . . . . : 10800 Сообщение . . . : Объект DAJOBCTL в *LIBL типа *DTAARA не найден. Причина . . . . : Не было найдено объекта с указанным именем или типом. Исправление . . : Укажите правильное имя или тип объекта. Затем повторите запрос.
Как видно из примера, в joblog'е отображается достаточно подробная информация о том, что, когда и где случилось.
И, наконец, собственно про RPG
RPG, в отличии от современных языков, не поддерживает языковых исключений. Т.е. в нем нет привычных try/catch/throw. Но, как уже показано выше, их нет не потому что это нереализуемо, но просто потому что в этом нет необходимости при наличии мощного и гибкого механизма системных исключений.
Возможность перехвата исключений в RPG существует уже достаточно давно. Аналогом try/catch является оператор monitor
monitor ; Procedure1() ; on-excp 'RNX1211' ; dsply ('RNX1211: Error in procedure') ; on-error 202 ; dsply ('Handled by 202') ; on-error ; dsply ('Catch all') ; endmon ;
Оператор monitor в данном случае то же самое что try, on-excp и on-error - catch
on-error работает со кодами ошибок (статусами). Есть списки Program Status Codes - статусы ошибок, связанные с работой программы и File Status Codes - статусы ошибок, связанные с доступом к файлам. Также возможно использование мнемоник - *PROGRAM - для всех программных статусов или *FILE для всех статусов, касающихся работы с файлами.
on-excp появилась относительно недавно, работает непосредственно с ID сообщения. Одновременно с on-excp появилась очень удобная команда snd-msg, позволяющая посылать сообщение в очередь сообщений программы (аналог throw). Таким образом, получаем полный аналог try/catch/throw с использованием механизма системных исключений
monitor; subproc (); on-excp 'ABC1234'; dsply 'on-excp ABC1234'; endmon; dcl-proc subproc; snd-msg *escape %msg('ABC1235': 'MYMSGF'); end-proc;
В данном случае предполагается что у нас есть файл сообщений MYMSGF в который мы записываем свои коды сообщений и мы предварительно записали туда сообщение с ID 'ABC1235'.
Далее, если у нас это действительно случилось в процедуре subproc, мы посылаем в очередь сообщений программы сообщение с ID 'ABC1235' описанное в файле MYMSGF.
При необходимости передачи дополнительной информации (параметров для подстановки в текст сообщения) блок параметров передается третьим параметром в %msg
snd-msg *escape %msg(dsError.errCode: 'KSMMSGF': dsError.errParmAll);
Чтобы он было перехвачено монитором, нужно указать, что это прерывающее сообщение - *escape.
Сообщение это будет перехвачено монитором и обработано в блоке on-excp 'ABC1234'.
Как уже писалось выше, можно посылать сообщение (исключение) не только себе, но и выше по стеку. Для этого у команды snd-msg есть дополнительный параметр %target позволяющий явно указывать куда посылать сообщение:
на текущий уровень стека - *SELF
на уровень стека выше - *CALLER
на произвольный уровень стека
Например, при необходимости послать сообщение на два уровня стека выше главной процедуры программы GFCLMTCMP пишем:
snd-msg *escape %msg(dsError.errCode: 'KSMMSGF': dsError.errParmAll) %target('GFCLMTCMP': 2);
Не перехваченные исключения
Отдельный вопрос - "а что будет если исключение возникло вне блока monitor?" На этот случай в RPG есть возможность описания специальной сабрутины со служебным именем *pssr, которая сама по себе является не обработчиком исключения, но точкой выхода из процедуры в случае возникновения внутри не перехваченного в monitor исключения.
Внутри *pssr уже можно сделать или полноценную обработку исключения, или, как минимум, запросить систему создать дамп где будет содержаться подробная информация о том, что и где пошло не так и получить значения всех переменных. В RPG для этого есть специальная команда dump.
При этом, если в *pssr будет присутствовать return (после всех необходимых действий), исключение будет считаться обработанным и программа завершится нормально. В отсутствие return исключение останется в статусе не обработаного и будет перколировать вверх по стеку вызовов.
Еще один важный момент - рекомендуется все действия по обработке исключения внутри *pssr оборачивать в блок monitor во избежание зацикливания в случае возникновения нового исключения в процессе обработки исходного.
dcl-proc mainProc; ... subprocA(...); ... return; begsr *pssr; // обрабатываем исключение endsr; end-proc; dcl-proc subprocA ... subprocB(...); ... return; begsr *pssr; dump; endsr; end-proc; dcl-proc subprocB ... return; begsr *pssr; dump; endsr; end-proc;
При возникновении исключения в subprocB будет создан дамп для этой процедуры. Но само исключение пойдет вверх по стеку и в subprocA также будет создан дамп. Дальше исключение попадет в mainProc где уже будет полноценно обработано в ее *pssr
В результате получим два дампа в которых будет полная информация о состоянии как subprocB так и вызвавшей ее subprocA которое привело к исключению. Т.е. максимально полная информация для последующего "разбора полетов".
Альтернативой *pssr является блок on-exit - единая точка выхода из процедуры. То место, когда попадаем после любого return или исключения.
dcl-proc someProc; dcl-s wasError ind; ... return; on-exit wasError; if wasError; // попали сюда после исключения ... endif; ... end-proc;
При возникновении исключения попадаем в on-exit с индикатором wasError = *on (устанавливается автоматически, достаточно просто его объявить и указать параметром для on-exit).
К сожалению, нельзя совместно использовать *pssr и on-exit, а сам on-exit достаточно ограничен по возможностям - там много чего делать нельзя. Например, нельзя использовать команду dump... Поэтому *pssr все-таки является более предпочтительным способом.
Чтобы понять что и где случилось если вдруг попали в *pssr, в RPG есть специальная структура - PSDS (Program Status Data Structure) которая объявляется глобальной переменной
dcl-ds dsRPGPS psds qualified ; Proc char(10) ; // Module or main procedure name StsCde zoned(5) ; // Status code PrvStsCde zoned(5) ; // Previous status SrcLineNbr char(8) ; // Source line number Routine char(8) ; // Name of the RPG routine Parms zoned(3) ; // Number of parms passed to program ExceptionType char(3) ; // Exception type ExceptionNbr char(4) ; // Exception number Exception char(7) samepos(ExceptionType) ; // Exception Message Id Reserved1 char(4) ; // Reserved MsgWrkArea char(30) ; // Message work area PgmLib char(10) ; // Program library ExceptionData char(80) ; // Retrieved exception data Rnx9001Exception char(4) ; // Id of exception that caused RNX9001 LastFile1 char(10) ; // Last file operation occurred on Unused1 char(6) ; // Unused DteEntered char(8) ; // Date entered system StrDteCentury zoned(2) ; // Century of job started date LastFile2 char(8) ; // Last file operation occurred on LastFileSts char(35) ; // Last file used status information JobName char(10) ; // Job name JobUser char(10) ; // Job user JobNbr zoned(6) ; // Job number StrDte zoned(6) ; // Job started date PgmDte zoned(6) ; // Date of program running PgmTime zoned(6) ; // Time of program running CompileDte char(6) ; // Date program was compiled CompileTime char(6) ; // Time program was compiled CompilerLevel char(4) ; // Level of compiler SrcFile char(10) ; // Source file name SrcLib char(10) ; // Source file library SrcMbr char(10) ; // Source member name ProcPgm char(10) ; // Program containing procedure ProcMod char(10) ; // Module containing procedure SrcLineNbrBin bindec(2) ; // Source line number as binary LastFileStsBin bindec(2) ; // Source id matching positions 228-235 User char(10) ; // Current user ExtErrCode int(10) ; // External error code IntoElements int(20) ; // Elements set by XML-INTO or DATA-INTO (7.3) InternalJobId char(16) ; // Internal job id (7.3 TR6) SysName char(8) ; // System name (7.3 TR6) end-ds ;
В объявлении должен присутствовать модификатор psds.
Здесь будет имя процедуры где произошло исключение (Proc), строка кода (SrcLineNbr), код сообщения (Exception) с блоком дополнительных данных (ExceptionData) и много другой полезной информации, позволяющей понять что и где произошло.
Механизм подавления исключений
Основной минус исключений в том, что это достаточно ресурсоемкий механизм. И использовать его в высоконагруженных системах нежелательно. К счастью, в RPG есть способ обхода исключений - модификатор (e) существующий для большинства операторов языка и преобразующий исключение в статус ошибки.
Например, для чтения файла:
read(e) ...; if %error; // случилась ошибка - обрабатываем endif;
В данном случае вместо того чтобы перехватывать исключение при помощи monitor используется модификатор (e) с последующей проверкой была ли ошибка.
Если ошибка была, можно получить ее код через функцию %status которая вернет код ошибки (аналогично errno в С). Для файловых операций будет возвращаться код ошибки из списка File Status Codes, для остальных ситуаций - из списка Program Status Codes.
С точки зрения нагрузки этот способ предпочтительнее перехвата исключения через monitor или *pssr.
Также этот механизм, равно как и перехват исключения при помощи monitor, позволяет избежать аварийного завершения в пользу контролируемой обработки ошибки.
Итого
Отсутствие в RPG языковых исключений с лихвой компенсируется наличием в IBM i исключений системных и поддержкой этих исключений средствами языка.
Единая концепция структурированной ошибки является универсальной и может использоваться как для системного исключения, так и для возврата ошибки через параметр.

