Продолжение цикла статей "что такое 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 исключений системных и поддержкой этих исключений средствами языка.

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