【実務・中級編】RECORD条件による入出力エラーのハンドリング – PL/Iの基本構文とデータ制御実践ガイド

後輩諸君、今日も一日お疲れ様。メインフレームのバッチ処理、それはまるで基幹システムの血管を巡る血液そのものだ。その流れが滞れば、ビジネスはたちまち麻痺する。特に、ファイルI/Oに関するエラーは、システムの健全性を脅かす最大の要因の一つ。長年この世界で飯を食ってきた私から見ても、I/Oエラーは一見単純そうに見えて、その裏に潜む問題は実に奥深い。

今回は、PL/Iプログラムにおける入出力エラーの象徴とも言える「RECORD条件」に焦点を当てる。ただエラーを出すだけでなく、「なぜ」エラーが発生したのか、その深淵をどうやって覗き込み、解決へと導くか。これまでの現場での苦い経験と、そこから培った確実なトラブルシューティングのノウハウを、実践的なコード例とともに伝授しよう。

RECORD条件とは何か? その深淵を覗く

PL/Iプログラムでファイルアクセスを行う際、我々は大きく分けて「STREAM I/O」と「RECORD I/O」の二つの方式を使い分ける。STREAM I/Oは文字データをストリームとして扱うのに対し、RECORD I/Oは物理的なレコード単位でデータをやり取りする。メインフレームの基幹バッチ処理で扱う固定長や可変長のデータファイル、VSAMファイルなどは、ほとんどがこのRECORD I/Oに分類される。

このRECORD I/Oにおいて、プログラムが予期せぬレコード形式や物理的な問題に遭遇した際に発生するのが「RECORD条件」だ。具体的には、以下のような状況で発生する。

  • レコード長不一致: プログラムで定義したレコード長と、実際にファイルから読み込んだ(または書き込もうとした)レコードの長さが異なる場合。これが一番よく見るパターンだ。
  • 物理的なI/Oエラー: ディスク障害、テープドライブの異常、読み取りヘッドの損傷など、物理的な問題によりデータの読み書きができない場合。
  • ファイル属性の不一致: JCLのDDステートメントで定義されたファイル属性(例: RECFORM=FB, LRECL=80)と、プログラム内で認識しているファイル属性が食い違っている場合。
  • VSAMファイルの破損: データセット自体が破損している、あるいはインデックスがおかしいなどの場合。

若手諸君が真っ青になるのは、開発環境では動いていたプログラムが、本番環境で突然このRECORD条件でコケた時だろう。「なぜだ!? テストは通ったはずなのに!」と叫ぶ君の顔が目に浮かぶ。その原因の多くは、開発環境と本番環境の「データ」や「JCLのDD定義」の差異に起因している。表面的なエラーメッセージだけでは、この深淵はなかなか見えてこない。

他のON条件との違い

PL/IにはRECORD条件の他にも様々なON条件がある。例えば、`ENDFILE`(ファイルの終端)、`KEY`(VSAMでのキー不一致)、`TRANSMIT`(データ伝送エラー)、`CONVERSION`(データ変換エラー)などだ。

RECORD条件は、これらのON条件の中でも特に「物理的なレコード構造」や「I/O操作の根幹」に関わる問題を示す。例えば、`KEY`条件はVSAMで特定のキーが見つからなかった場合だが、`RECORD`条件はそれ以前にレコード自体を正しく読み書きできない、というより深刻な状況を示すことが多い。つまり、RECORD条件が発生した時点で、そのレコードは「データとして成り立たない」可能性が高いということだ。

現場を震撼させるRECORD条件の罠

私が経験した中でも、RECORD条件にまつわるトラブルは数知れない。ある日、深夜の本番バッチがRECORD条件で停止し、顧客からのクレームが殺到したことがある。原因は、前日の別システムからの連携ファイルが、仕様変更でレコード長が変わっていたにもかかわらず、バッチプログラム側の定義が更新されていなかった、という単純なものだった。しかし、その特定には数時間かかり、多大な損害を生んだ。

こんな時、君はプログラムのエラーメッセージを見て、ただ「I/Oエラー」とだけ表示されていることに絶望するだろう。もっと具体的な情報が欲しい。どのファイルで? どんな内容のレコードで? 何が原因で?

PL/IのONユニットと、それに付随するBUILTIN関数、そして最終兵器「PLIDUMP」を駆使すれば、この謎を解き明かすことができる。

RECORD条件を捕捉し、システムを護るONユニット

PL/Iの`ON`ステートメントは、特定の条件が発生した際に実行される処理(ONユニット)を定義するための強力な機能だ。RECORD条件も例外ではない。適切に`ON RECORD`ユニットを設定することで、プログラムの異常終了を防ぎ、エラーの原因を詳細にログに出力したり、場合によってはエラーを回避して処理を続行したりすることができる。

ONユニット内で何をするべきか?

ONユニットの目的は、単にエラーをトラップするだけではない。それは「エラーの発生源と状況を特定し、適切な処置を施すこと」にある。

1. エラー情報の取得:

  • `ONCODE`:発生した条件の数値コード。RECORD条件であれば、通常1000番台のコードが返される(例: 1000はレコード長不一致、1001は物理I/Oエラー)。
  • `ONFILE`:エラーが発生したファイル名。
  • `ONSOURCE`:エラー発生時のI/Oバッファの内容。これが非常に重要で、不正なレコードの内容を直接見ることができる。
  • `ONKEY`:VSAMファイルの場合、エラー発生時のキー値。

2. 適切なエラーメッセージの出力:

  • SYSPRINTやエラーログファイルに、取得した詳細情報を出力する。いつ、どのファイルで、どんなONCODEで、どんな内容のレコードでエラーが発生したのかを明確にする。

3. 異常終了処理、または回復処理:

  • 致命的なエラーであれば、プログラムを異常終了させる。この際、`CALL PLIDUMP`を使ってダンプを出力させ、詳細なデバッグ情報(レジスタ、スタック、メモリ内容など)を取得するのが常套手段だ。
  • エラーの種類によっては、そのレコードをスキップして処理を続行する、という選択肢もある。ただし、これはデータ整合性に影響を与える可能性があるため、非常に慎重に判断する必要がある。安易なスキップは、後でより大きな問題を引き起こす地雷原となることがあるので注意が必要だ。

実践! RECORD条件ハンドリングのPL/Iコード例

それでは、具体的なPL/Iコードを見てみよう。入力ファイルを読み込み、RECORD条件が発生した際に詳細なエラー情報を出力し、最終的にPLIDUMPを呼び出して異常終了する、というシナリオだ。

EXAMPLE: PACKAGE OPTIONS(MAIN);

/ ——————————————————————— /
/ DCL SECTION: 変数とファイルの定義 /
/ ——————————————————————— /
DCL SYSIN FILE INPUT SEQUENTIAL ENV(F RECSIZE(80)); / 入力ファイル (LRECL=80) /
DCL SYSPRINT FILE OUTPUT PRINT ENV(F RECSIZE(133)); / 出力ファイル (SYSOUT) /
DCL ERRORLOG FILE OUTPUT SEQUENTIAL ENV(F RECSIZE(200)); / エラーログファイル /

DCL 1 INPUT_REC, / 入力レコード構造体 (LRECL=80) /
2 REC_ID CHAR(10), / レコードID /
2 REC_DATA CHAR(70); / データ部 /

DCL 1 ERROR_REC, / エラーログレコード構造体 (LRECL=200) /
2 ERR_TIME CHAR(19), / 発生日時 (YYYY-MM-DD-HH.MM.SS) /
2 ERR_FILE CHAR(8), / エラー発生ファイル名 /
2 ERR_ONCODE CHAR(5), / ONCODE値 /
2 ERR_MESSAGE CHAR(100),/ エラーメッセージ /
2 ERR_ONSOURCE CHAR(60),/ ONSOURCEの内容(一部)/
2 FILLER CHAR(8); / 埋め草 /

DCL ABEND_MSG CHAR(256) VARYING; / 異常終了メッセージ /
DCL L_ONCODE FIXED BIN(15); / ONCODE値を格納するローカル変数 /

/ ——————————————————————— /
/ ON UNIT SECTION: 各種ON条件の定義 /
/ ——————————————————————— /

/ ファイル終端条件 (ENDFILE) のハンドリング /
ON ENDFILE(SYSIN)
BEGIN;
PUT SKIP LIST(‘INFO: ファイルSYSINの終端に達しました。処理を終了します。’);
GO TO CLOSE_FILES; / ファイルクローズ処理へジャンプ /
END;

/ RECORD条件のハンドリング – ここが本命! /
ON RECORD(SYSIN)
BEGIN;
/ RECORD条件発生時の処理を開始 /
L_ONCODE = ONCODE; / 発生したONCODE値を取得 /

PUT FILE(SYSPRINT) SKIP LIST(‘ CRITICAL ERROR: RECORD条件が発生しました ‘);
PUT FILE(SYSPRINT) SKIP LIST(‘ ONCODE : ‘ || L_ONCODE);
PUT FILE(SYSPRINT) SKIP LIST(‘ ファイル名 : ‘ || ONFILE);
PUT FILE(SYSPRINT) SKIP LIST(‘ ONSOURCE : ”’ || ONSOURCE || ””); / ONSOURCEの内容を出力 /
PUT FILE(SYSPRINT) SKIP LIST(‘ ‘); / 空行 /

/ ONCODE値に応じて、具体的なエラーメッセージを生成 /
SELECT (L_ONCODE);
WHEN (1000) / レコード長不一致 /
DO;
ABEND_MSG = ‘レコード長不一致エラー。プログラム定義のLRECL=’ || RECSIZE(SYSIN) || ‘に対して、実際に読み込んだレコード長が異なります。’;
PUT FILE(SYSPRINT) SKIP LIST(‘ 原因 : ‘ || ABEND_MSG);
CALL WRITE_ERROR_LOG(ONFILE, L_ONCODE, ABEND_MSG, ONSOURCE);
CALL ABEND_PROGRAM(ABEND_MSG); / 異常終了プロシージャ呼び出し /
END;
WHEN (1001) / 物理I/Oエラー (OPEN時など) /
DO;
ABEND_MSG = ‘物理I/Oエラーが発生しました。ディスク障害やファイル破損の可能性があります。’;
PUT FILE(SYSPRINT) SKIP LIST(‘ 原因 : ‘ || ABEND_MSG);
CALL WRITE_ERROR_LOG(ONFILE, L_ONCODE, ABEND_MSG, ONSOURCE);
CALL ABEND_PROGRAM(ABEND_MSG); / 異常終了プロシージャ呼び出し /
END;
OTHERWISE / その他のRECORD条件 /
DO;
ABEND_MSG = ‘不明なRECORD条件が発生しました。ONCODE=’ || L_ONCODE || ‘。詳細を確認してください。’;
PUT FILE(SYSPRINT) SKIP LIST(‘ 原因 : ‘ || ABEND_MSG);
CALL WRITE_ERROR_LOG(ONFILE, L_ONCODE, ABEND_MSG, ONSOURCE);
CALL ABEND_PROGRAM(ABEND_MSG); / 異常終了プロシージャ呼び出し /
END;
END;
END; / ON RECORD END /

/ ——————————————————————— /
/ SUB-PROCEDURES: サブプロシージャ群 /
/ ——————————————————————— /

/ エラーログ書き込みプロシージャ /
WRITE_ERROR_LOG: PROCEDURE(P_FILE_NAME CHAR(8), P_ONCODE FIXED BIN(15), P_MESSAGE CHAR(), P_ONSOURCE CHAR());
ERROR_REC.ERR_TIME = DATETIME(); / 現在日時取得 /
ERROR_REC.ERR_FILE = P_FILE_NAME;
ERROR_REC.ERR_ONCODE = P_ONCODE;
ERROR_REC.ERR_MESSAGE = SUBSTR(P_MESSAGE, 1, 100); / メッセージ長を調整 /
ERROR_REC.ERR_ONSOURCE = SUBSTR(P_ONSOURCE, 1, 60); / ONSOURCEも記録 /
WRITE FILE(ERRORLOG) FROM(ERROR_REC); / エラーログファイルに出力 /
END WRITE_ERROR_LOG;

/ 異常終了プロシージャ /
ABEND_PROGRAM: PROCEDURE(P_ABEND_MSG CHAR());
PUT FILE(SYSPRINT) SKIP LIST(‘ ‘);
PUT FILE(SYSPRINT) SKIP LIST(‘@@@ CRITICAL ABEND: プログラムを異常終了します @@@’);
PUT FILE(SYSPRINT) SKIP LIST(‘ メッセージ: ‘ || P_ABEND_MSG);
PUT FILE(SYSPRINT) SKIP LIST(‘ ‘);
CALL PLIDUMP(P_ABEND_MSG, ‘USER’); / PLIDUMPを呼び出してダンプを出力 /
STOP; / プログラムを即時終了 /
END ABEND_PROGRAM;

/ ——————————————————————— /
/ MAIN LOGIC: メイン処理ロジック /
/ ——————————————————————— /

/ ファイルオープン /
OPEN FILE(SYSIN) INPUT;
OPEN FILE(SYSPRINT) OUTPUT;
OPEN FILE(ERRORLOG) OUTPUT;

PUT FILE(SYSPRINT) SKIP LIST(‘INFO: プログラム処理を開始します。’);

/ メイン処理ループ /
DO WHILE(‘1’B); / 無限ループ、ENDFILE条件でGO TO CLOSE_FILESへ抜ける /
READ FILE(SYSIN) INTO(INPUT_REC); / レコードを読み込み /
/ ここに正常処理ロジックを記述 /
PUT FILE(SYSPRINT) SKIP LIST(‘処理中レコード: ‘ || INPUT_REC.REC_ID);
/ 意図的にレコード長不一致を発生させるための例 (テスト用) /
/ IF SUBSTR(INPUT_REC.REC_ID, 1, 3) = ‘ERR’ THEN SIGNAL RECORD; /
END;

CLOSE_FILES: / ファイルクローズラベル /
CLOSE FILE(SYSIN);
CLOSE FILE(SYSPRINT);
CLOSE FILE(ERRORLOG);

PUT FILE(SYSPRINT) SKIP LIST(‘INFO: 処理が正常終了しました。’);

END EXAMPLE;

コード解説のポイント

  • `ON RECORD(SYSIN)`: `SYSIN`ファイルに対してRECORD条件が発生した場合に、`BEGIN; … END;`ブロック内の処理が実行される。
  • `ONCODE`: このBUILTIN関数で、発生したRECORD条件の具体的なコードを取得できる。今回は`1000`(レコード長不一致)と`1001`(物理I/Oエラー)を例に挙げたが、IBMのPL/I Language Reference Manualで詳細なリストを確認してほしい。
  • `ONFILE`: どのファイルでエラーが発生したかを返してくれる。複数のファイルを扱うプログラムでは必須の情報だ。
  • `ONSOURCE`: これがレコード長不一致の原因特定に最も役立つ情報の一つだ! エラーが発生したI/Oバッファの内容を文字列として返してくれる。例えば、プログラムがLRECL=80を期待しているのに、ONSOURCEがLRECL=70のデータを示していれば、一目瞭然でレコード長不一致が疑われるだろう。
  • `WRITE_ERROR_LOG` プロシージャ: エラー情報を専用のログファイルに出力することで、SYSPRINTが流れてしまっても後から確認できるようにする。これは本番システムでは必須の対応だ。`DATETIME()` BUILTIN関数で日時を記録している。
  • `ABEND_PROGRAM` プロシージャ: 異常終了処理を独立したプロシージャにまとめることで、コードの見通しを良くし、再利用性を高めている。この中で`CALL PLIDUMP`を実行するのがミソだ。

ダンプ出力からの原因特定:PLIDUMPの活用

`CALL PLIDUMP(タイトル文字列, オプション文字列)` は、PL/Iプログラムのデバッグにおいてまさに「最終兵器」と呼ぶにふさわしい機能だ。これが呼び出されると、JCLで定義された`SYSDUMP`または`SYSUDUMP`に出力されるシステムダンプに加えて、PL/Iの実行環境に関する詳細な情報(レジスタの内容、PSW、セーブエリアチェーン、ファイル制御ブロック、変数の値など)が整形されて出力される。

ダンプの読み方(RECORD条件の場合)

PLIDUMPが出力する内容は膨大だが、RECORD条件で重要なのは以下の点だ。

1. WHERE WAS THE PROGRAM EXECUTING? (PSW):
PLIDUMPの出力冒頭にあるPSW(Program Status Word)やレジスタ内容から、ダンプ発生時のプログラムの正確な実行位置(オフセットアドレス)を特定できる。これにより、どの`READ`または`WRITE`ステートメントでRECORD条件が発生したかを絞り込める。

2. FILE CONTROL BLOCKS (ACB/RPL for VSAM, DCB for QSAM):
開いているファイルに関する情報がダンプ内に出力される。特に注目すべきは、ファイル制御ブロック(例えばQSAMならDCB、VSAMならACBやRPL)の内容だ。

  • DCB: `LRECL`(論理レコード長)、`BLKSIZE`(ブロックサイズ)、`RECFORM`(レコード形式)などの情報が格納されている。プログラム内で定義したレコード長と、DCB内の`LRECL`が一致しているかを確認する。もし不一致であれば、JCLのDDステートメントやデータセットの属性に問題がある可能性が高い。
  • ACB/RPL (VSAM): VSAMファイルの場合、ACB(Access Method Control Block)やRPL(Request Parameter List)の内容を見る。これらのブロックには、キー長、相対キー位置、レコード長などのVSAM固有の属性が格納されている。`KEY`条件と合わせて確認することで、VSAMファイルに対する操作が適切だったか、ファイル自体が破損していないかなどを判断できる。

3. PROGRAM VARIABLES:
ダンプには、プログラム内で宣言された変数の値も出力される。特に、`ONSOURCE`で取得した内容が、ダンプのメモリ領域(I/Oバッファ)と一致するかを確認することで、エラーが発生した時点の生のデータを確認できる。
もし`ONSOURCE`が空や意味不明な文字列だったとしても、ダンプのメモリダンプ領域からI/Oバッファを探し出し、不正なレコードの内容を直接目で見て確認できる。そこから、本来あるべきレコード長より短い、あるいは途中で不正な文字が入っている、などの手がかりを見つけ出すのだ。

ダンプの読み解きは一朝一夕には身につかないが、上記のようなポイントを意識して繰り返し訓練すれば、必ずや君の強力な武器となるだろう。

よくある落とし穴とデバッグのコツ

1. ONユニット内でのエラーの連鎖

ONユニット内でさらにI/Oエラーやデータ変換エラーが発生した場合、どうなるか? PL/Iはデフォルトで再帰的にONユニットを呼び出す挙動をする。つまり、ONユニット内で`PUT`や`WRITE`を行っている際に、その出力処理でまたI/Oエラーが発生すると、新たなONユニットが呼び出され、無限ループに陥る可能性がある。これを避けるためには、ONユニット内でのI/O処理は最小限にとどめるか、あるいは`NOCHECK`オプションを検討する必要がある。通常は、`SYSPRINT`への出力は安全だが、エラーログファイルへの書き込みは慎重に行うべきだ。

2. `PUT DATA`の活用

デバッグ時に一時的に変数の内容を確認したい場合は、`PUT DATA(変数名);` ステートメントが非常に便利だ。変数の名前と値が自動的にSYSPRINTに出力される。ONユニットに入る直前や、怪しい処理の前後に入れておくと良い。

3. JCLのMSGLEVELとダンプオプション

  • `//GO.SYSOUT DD SYSOUT=` だけでなく、`MSGLEVEL=(1,1)` を指定してJCLの実行状況を詳細に出力させたり、`//GO.SYSDUMP DD SYSOUT=` や `//GO.SYSUDUMP DD SYSOUT=` を追加してシステムダンプを確実に出力させることで、プログラム以外の要因(JCLエラー、ファイルのアロケーションエラーなど)も確認できる。

4. テストデータの網羅性

開発環境でRECORD条件が発生しないのは、テストデータが「正常系」ばかりだからだ。異常なレコード長を持つデータ、途中に不正な文字が混入したデータ、ファイルの終端が正しくないデータなど、様々な「異常系」データを意図的に作成し、テストシナリオに組み込むことが、本番でのトラブルを未然に防ぐ最善策だ。

5. 目視の限界とツールの活用

大量のデータの中から不正なレコードを目視で探すのは非効率的だ。`FILE-AID`や`SORT`ユーティリティなど、メインフレームには強力なデータ操作・分析ツールが揃っている。これらを活用して、レコード長が異なるデータだけを抽出したり、特定のパターンを持つレコードを探したりすることで、原因特定までの時間を大幅に短縮できる。

まとめ

RECORD条件は、メインフレームのPL/Iバッチ処理において、避けては通れない重要なエラー条件だ。しかし、適切に`ON RECORD`ユニットを設定し、`ONCODE`、`ONFILE`、そして特に`ONSOURCE`といったBUILTIN関数を駆使してエラーの詳細情報を取得し、最終的には`PLIDUMP`で深層のデバッグ情報を引き出すことで、どんなに複雑な原因も必ず突き止めることができる。

単にエラーをトラップするだけでなく、「なぜ」そのエラーが発生したのか、その真の原因を突き止める探求心こそが、君を真のメインフレームエンジニアへと成長させる。この知識を血肉とし、現場で頼られる、そしてシステムを護り抜く頼もしいエンジニアになってほしい。

健闘を祈る!

タイトルとURLをコピーしました