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

皆さん、こんにちは!世界最高峰のIBMメインフレームシステムアーキテクトにして、PL/Iのディープな世界をこよなく愛するレガシー移行スペシャリストの〇〇です。

新しい言語、特に「メインフレームのPL/I」と聞くと、ちょっと身構えてしまいますよね?「なんだか難しそう」「古いシステムの話でしょ?」なんて思われるかもしれません。でも、ご安心ください!JavaやCOBOLの経験がある皆さんなら、その基礎知識を土台に、PL/Iの奥深い世界もきっと楽しめます。そして何より、現代の基幹システムを支えるPL/Iは、今も現役バリバリで稼働しているんです。

今回は、そんなPL/Iのプログラムを動かす上で避けて通れない、そして非常に重要なテーマ、「RECORD条件による入出力エラーのハンドリング」について、現場のリアルな知見を交えながら、優しく紐解いていきたいと思います。

PL/Iプログラムの基本構造をサクッとおさらい

まずは、PL/Iプログラムの「骨格」について、軽く触れておきましょう。Javaでいうところの`public static void main(String[] args)`、COBOLでいうところの`IDENTIFICATION DIVISION`や`PROCEDURE DIVISION`に相当する部分ですね。

SAMPLE: PACKAGE OPTIONS(MAIN);

/ ここに変数宣言やファイル宣言などを記述します /
DCL SOME_VARIABLE FIXED BINARY(31);

/ ここに処理ロジックを記述します /
PUT SKIP LIST(‘Hello, PL/I World!’);

END SAMPLE;

  • `PACKAGE`:
  • これはPL/Iのプログラムの「入れ物」のようなものです。関連する手続き(`PROCEDURE`)やデータ定義をひとまとめにする役割があります。Javaでいうパッケージや、C#の名前空間に近い感覚ですね。
  • `OPTIONS(MAIN)`:
  • この`PROCEDURE`が、プログラムの「開始点」であることを示します。OSから最初に実行されるエントリポイントになる、という意味です。Javaの`main`メソッドと全く同じ役割ですよ。
  • `END SAMPLE;`:
  • プログラムの終わりを示します。COBOLの`END PROGRAM`やJavaのブロック閉じ括弧`}`と同じです。

このように、PL/Iのプログラムは、`PACKAGE`で始まり、`OPTIONS(MAIN)`を持つ`PROCEDURE`がエントリポイントとなり、最後に`END`で閉じられる、というシンプルな構造をしています。

今回はこの基本構造の中に、入出力エラーを華麗にハンドリングする技を組み込んでいきますよ!

恐怖の「RECORD条件」とは?宅配便の荷物で例えてみよう

さて、本題の「RECORD条件」です。この条件は、プログラムがファイルからデータを読み書きする際、特にレコードの形式に関する問題が発生したときに発動します。

想像してみてください。あなたは宅配便の配達員で、荷物(レコード)を配達する(読み書きする)のが仕事です。

1. レコード長不一致:

  • あなたは「縦80cm、横50cmの箱」が届くはずだと思って構えていました。ところが実際に来たのは「縦100cm、横30cmの箱」だった!箱のサイズが事前に聞いていたものと全然違う、これでは配達トラックに積めない!
  • PL/Iで言えば、プログラム側では「80バイトのレコード」として定義しているのに、実際にファイルに格納されているレコードが「100バイト」だったり、「60バイト」だったりするケースです。これはファイル定義ミスや、連携元のシステムでレコード長が変わった際によく発生します。

2. 物理的なI/Oエラー:

  • 荷物を積んだトラックが、配達途中でパンクしてしまった!あるいは、倉庫のシャッターが壊れて荷物が出せない!
  • これは、ディスク装置の故障や、ファイル自体が破損している、あるいはファイルにアクセスする権限がない、といった物理的な入出力エラーを指します。

このように、RECORD条件は「事前に想定していたデータ形式や、物理的な入出力の前提が崩れた!」という、非常に深刻な事態を教えてくれるアラートなんです。これを放置すると、プログラムは正常に動作せず、ABEND(異常終了)してしまいます。基幹システムのバッチ処理でABENDが発生すると、その後の関連処理も止まってしまい、業務に大きな影響を与えかねません。

だからこそ、このRECORD条件を適切にハンドリングすることが、プロのPL/Iプログラマーには必須のスキルになるんです。

RECORD条件の捕捉方法:ON CONDITIONステートメント

Javaで例外を捕捉するために`try-catch`を使うように、PL/Iには「`ON CONDITION`ステートメント」という強力な武器があります。特定の条件が発生したときに実行する処理を、あらかじめ定義しておくことができるんです。

RECORD条件を捕捉するには、`ON RECORD`を使います。

ON RECORD(ファイル名)
BEGIN;
/ RECORD条件が発生したときに実行する処理をここに記述 /
/ 例えば、エラーメッセージの出力、ログ記録、ダンプ出力など /
END;

どうでしょう?COBOLの`ON SIZE ERROR`や`INVALID KEY`に似ている、と感じた方もいるのではないでしょうか。特定のイベントをトリガーとして処理を実行する、という考え方は共通していますよね。

この`ON RECORD`ブロックの中には、エラーの状況を把握するための様々な情報が詰まっています。特に重要なのが、`ONCODE`と`ONFILE`です。

  • `ONCODE`: どのようなエラーが発生したのかを示す数値コードです。このコードを見ることで、具体的に何が起こったのかを判断できます。例えば、レコード長不一致であれば特定の`ONCODE`が返されます。
  • `ONFILE`: どのファイルでエラーが発生したのかを示すファイル名です。複数のファイルを扱っているプログラムでは、これで原因の切り分けができます。

これらの情報を活用して、エラー発生時に適切な処理を行い、原因特定のための情報を出力する。これがPL/Iにおけるエラーハンドリングの肝になります。

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

それでは、実際に`ON RECORD`を使って入出力エラーをハンドリングするPL/Iプログラムを見ていきましょう。今回は、入力ファイルからレコードを読み込み、レコード長不一致が発生した場合にエラーを捕捉し、ダンプを出力してプログラムを終了する、というシナリオを想定します。

SAMPLE: PACKAGE OPTIONS(MAIN);

/ ファイル宣言 /
DCL SYSIN FILE INPUT RECORD ENVIRONMENT(F(80)); / 入力ファイル: 固定長80バイト /
DCL SYSOUT FILE OUTPUT PRINT; / 出力ファイル: 処理結果などを表示 /
DCL SYSADATA FILE OUTPUT RECORD ENVIRONMENT(F(1024)); / ダンプ出力用ファイル: 後述のJCLでSYSOUT=を指定してログに出すことが多い /

/ 入力レコードの構造体定義 /
/ プログラムは、この構造体でレコードを受け取ることを期待しています /
DCL 1 INPUT_REC, / レコードバッファ (構造体) /
2 REC_ID CHAR(5), / レコードID (先頭5バイト) /
2 REC_DATA CHAR(75); / データ部 (続く75バイト) /
/ 合計 5 + 75 = 80 バイト /

/ 制御フラグとカウンタ /
DCL EOF_SW BIT(1) INIT(‘0’B); / ファイル終端フラグ (‘0’B: False, ‘1’B: True) /
DCL ERROR_FOUND BIT(1) INIT(‘0’B); / エラー検出フラグ /
DCL REC_COUNT FIXED BINARY(31) INIT(0); / 処理レコード数カウンタ /

/ — ここからが重要!条件ハンドリングの定義 — /

/ ファイル終端条件 (ENDFILE) のハンドリング /
ON ENDFILE(SYSIN)
EOF_SW = ‘1’B; / ファイル終端時にフラグを立ててループを抜ける /

/ RECORD条件発生時のハンドリング /
ON RECORD(SYSIN) BEGIN; / SYSINファイルでRECORD条件が発生したらこのブロックを実行 /
PUT FILE(SYSOUT) SKIP LIST(‘ RECORD CONDITION OCCURRED! ‘); / 画面(SYSOUT)にエラーメッセージを出力 /
PUT FILE(SYSOUT) SKIP LIST(‘ ONCODE = ‘ || ONCODE); / 発生したONCODEを出力 /
PUT FILE(SYSOUT) SKIP LIST(‘ ONFILE = ‘ || ONFILE); / エラーが発生したファイル名を出力 /

/ SYSADATAファイルへダンプ出力(原因特定のための超重要情報!) /
PUT FILE(SYSADATA) SKIP LIST(‘— DUMP INFORMATION FOR RECORD CONDITION —‘);
PUT FILE(SYSADATA) SKIP DATA(REC_COUNT); / 何件目でエラーが発生したかを出力 /
PUT FILE(SYSADATA) SKIP LIST(‘ Input Buffer Content (Raw Data):’);
PUT FILE(SYSADATA) SKIP STRING(INPUT_REC); / レコードバッファの内容をそのまま出力 /
PUT FILE(SYSOUT) SKIP LIST(‘ Detailed dump information written to SYSADATA.’);

ERROR_FOUND = ‘1’B; / エラーフラグを立てる /
GO TO PROGRAM_END; / 処理を終了させる (後述のラベルへジャンプ) /
END;

/ その他の一般的なエラー捕捉用 (RECORD条件以外) /
ON ERROR BEGIN;
PUT FILE(SYSOUT) SKIP LIST(‘ GENERAL ERROR OCCURRED! (ON ERROR) ‘);
PUT FILE(SYSOUT) SKIP LIST(‘ ONCODE = ‘ || ONCODE);
PUT FILE(SYSADATA) SKIP LIST(‘— DUMP INFORMATION FOR GENERAL ERROR —‘);
PUT FILE(SYSADATA) SKIP DATA(REC_COUNT);
PUT FILE(SYSADATA) SKIP STRING(INPUT_REC);
PUT FILE(SYSOUT) SKIP LIST(‘ Detailed dump information written to SYSADATA.’);
ERROR_FOUND = ‘1’B;
GO TO PROGRAM_END;
END;

/ — メイン処理ループ — /
MAIN_LOOP: DO WHILE(EOF_SW = ‘0’B & ERROR_FOUND = ‘0’B);
REC_COUNT = REC_COUNT + 1; / 処理レコード数をカウントアップ /

/ ファイルからレコードを読み込む /
/ ここでレコード長不一致などが発生すると、上記の ON RECORD(SYSIN) ブロックに処理が移ります /
READ FILE(SYSIN) INTO(INPUT_REC);

IF EOF_SW = ‘0’B THEN DO; / ファイル終端でなければ正常処理を続ける /
PUT FILE(SYSOUT) SKIP LIST(REC_COUNT || ‘: ‘ || INPUT_REC.REC_ID || ‘ – ‘ || INPUT_REC.REC_DATA);
/ ここに正常時の業務ロジックを記述します /
/ 例: データベース更新、計算処理、別のファイルへの書き出しなど /
END;
END; / DO WHILE の終わり /

/ — プログラム終了処理 — /
PROGRAM_END:; / 処理終了を示すラベル (GO TO のジャンプ先) /
PUT FILE(SYSOUT) SKIP LIST(‘— PROGRAM END —‘);
IF ERROR_FOUND = ‘1’B THEN
PUT FILE(SYSOUT) SKIP LIST(‘ Program terminated due to an error condition.’);
ELSE
PUT FILE(SYSOUT) SKIP LIST(‘ Processed ‘ || (REC_COUNT – 1) || ‘ records successfully.’);

END SAMPLE; / PACKAGE の終わり /

コード解説のポイント

  • `DCL 1 INPUT_REC, 2 REC_ID CHAR(5), 2 REC_DATA CHAR(75);`:
  • これはPL/Iの「構造体(Structure)」宣言です。COBOLの`01-level`やJavaのクラス定義に近いですね。`INPUT_REC`という名前の構造体の中に、`REC_ID`(5文字)と`REC_DATA`(75文字)という2つのフィールドがある、と定義しています。プログラムはこの「80バイト」のレコード構造を期待しています。
  • `ENVIRONMENT(F(80))`:
  • ファイル宣言の`ENVIRONMENT`属性は、メインフレームのJCL(Job Control Language)で定義されるデータセットの特性とプログラムを結びつける重要な部分です。`F(80)`は「固定長(Fixed-length)で、1レコードの長さが80バイト」であることを示します。もしJCLで定義されている実際のレコード長とこの`F(80)`が異なると、RECORD条件が発生する原因の一つになります。
  • `SYSADATA`:
  • これはPL/Iのデバッグ情報を出力するための特殊なファイルです。JavaのログファイルやCOBOLの`SYSOUT`に似ていますが、より構造化されたデバッグ情報を出力できます。エラー発生時の「ダンプ出力」に非常に役立ちます。
  • `PUT FILE(SYSADATA) SKIP DATA(REC_COUNT);`:
  • `PUT DATA`は、指定した変数の「変数名 = 値」という形式で出力してくれる便利な機能です。エラー発生時の変数状況を把握するのに役立ちます。
  • `PUT FILE(SYSADATA) SKIP STRING(INPUT_REC);`:
  • `PUT STRING`は、指定した変数の「内容」をそのまま出力します。`INPUT_REC`という構造体全体の内容を、バイト列として出力できるため、レコード長不一致などで「実際にはどんなデータが来たのか」をバイナリレベルで確認するのに非常に有用です。
  • `GO TO PROGRAM_END;`:
  • 現代のプログラミングではあまり推奨されない`GO TO`ですが、PL/Iの特に古いコードではよく使われます。エラー発生時に処理を中断し、プログラムの終了処理へジャンプさせるために利用されます。

ダンプ出力による原因特定手法:SYSADATAを読み解く

エラーを捕捉するだけでは不十分です。最も重要なのは、「なぜエラーが起きたのか」を特定し、解決することですよね。ここで威力を発揮するのが、先ほどのコードで登場した`SYSADATA`ファイルへのダンプ出力です。

`SYSADATA`は、通常、JCLで`SYSOUT=`として、ジョブログの一部として出力されることが多いです。このファイルには、プログラムの実行情報、変数の値、そしてエラー発生時のバッファ内容などが記録されます。

レコード長不一致の場合の`SYSADATA`の読み方

例えば、先ほどのプログラムが80バイトのレコードを期待しているのに、実際にはファイルに100バイトのレコードが入っていたとしましょう。`ON RECORD`が捕捉され、`SYSADATA`には以下のような情報が出力されます。

1. `REC_COUNT = 1234;`:

  • 「1234件目のレコードでエラーが発生したんだな」と分かります。

2. `Input Buffer Content (Raw Data):` の後のバイト列:

  • `PUT STRING(INPUT_REC)`で出力された、エラー発生時の入力バッファの生データがここに表示されます。
  • プログラムは80バイトの`INPUT_REC`として受け取ろうとしましたが、実際には100バイトのデータが流れ込んできた場合、この`PUT STRING`では、その時点でのバッファ内容(例えば先頭の80バイト)が出力されます。
  • この生データをバイナリエディタなどで確認することで、「あれ?このデータ、80バイトじゃないぞ、後ろにも何かある!」といった発見につながります。

`ONCODE`で何がわかる?

`ONCODE`は非常に多くの種類があり、それぞれのコードが特定のエラーを示します。IBMのPL/Iメッセージおよびコードマニュアルには詳細が記載されていますが、代表的な`ONCODE`をいくつかご紹介しましょう。

  • 10000: レコード長不一致(多くの場合、定義されたレコード長と実際のファイルレコード長が異なる)
  • 10001: ブロック化されたファイルのレコード長がブロック長を超過
  • 10002: 固定長レコードファイルで、READ時に定義されたレコード長と異なる長さのレコードを検出

`ONCODE`の値は、まさにエラーの「診断書」です。これを見ることで、「ファイル定義が間違っているのか?」「データに異常があるのか?」といった具体的な原因を絞り込むことができます。

現場でのトラブルシューティングのコツ

私がこれまで数多くのPL/Iシステムを見てきた中で、`RECORD`条件に関するトラブルシューティングの経験から、いくつかアドバイスさせてください。

1. JCLのファイル定義を疑うべし!:

  • 最も多いのが、PL/Iプログラム内の`ENVIRONMENT(F(80))`のような定義と、JCLの`DCB=(RECFM=FB,LRECL=80,BLKSIZE=…)`のようなデータセット定義が一致していないケースです。プログラムを改修した際にJCLの変更を忘れた、他システムからの連携ファイルの定義が変わった、などが典型的なパターンです。

2. `ON RECORD`を定義し忘れない!:

  • 初心者の方によくあるのが、`ON RECORD`を定義しないままI/O処理を行い、エラーが発生すると即座にABENDしてしまうことです。特に、今までエラーが出なかったファイルでも、いつ何時データが変わるか分かりません。重要なI/O処理には必ず`ON RECORD`で防御壁を張りましょう。

3. `PUT STRING`の活用はマスト!:

  • `RECORD`条件で「何を読もうとして、実際に何が来たのか」を知るには、`PUT STRING`によるバッファの生データ出力が非常に役立ちます。これを怠ると、エラーメッセージだけでは原因特定に時間がかかり、最悪の場合、デバッグのために何度もプログラムを再実行することになります。

4. `ON ENDFILE`との併用:

  • ファイル終端(`ENDFILE`)とレコード条件(`RECORD`)は、I/O処理における主要なイベントです。必ず両方をハンドリングするようにしましょう。

5. テストデータで再現性を確認:

  • 本番で発生した`RECORD`条件は、原因を特定するために、可能な限りテスト環境で再現させることが重要です。実際に不正なレコード長を持つファイルを作成し、`ON RECORD`が期待通りに動作するかを確認しましょう。

PL/Iのプログラムは、メインフレームという非常に安定した環境で動作していますが、外部からの入力データは常に変化する可能性があります。そのため、このような堅牢なエラーハンドリングは、システムの安定稼働を支える上で不可欠な要素なんです。

まとめ

今回は、PL/Iプログラムにおける「RECORD条件」のハンドリングについて、その重要性から具体的なコード例、そしてトラブルシューティングのコツまで、幅広く解説してきました。

  • RECORD条件は、レコード長不一致や物理的なI/Oエラーなど、入出力データの形式や環境に関する問題が発生したときに発生します。
  • `ON RECORD(ファイル名)`ステートメントを使って、これらのエラーを捕捉し、適切な処理を行うことができます。
  • `ONCODE`や`ONFILE`でエラーの詳細を把握し、`SYSADATA`ファイルへのダンプ出力(`PUT STRING`など)で具体的な原因を特定することが、スムーズな問題解決への鍵です。

メインフレームやPL/Iの世界は、一見するととっつきにくいかもしれませんが、その根底にある考え方は現代のプログラミング言語と共通する部分も多くあります。そして何より、半世紀近くもの間、社会の基盤を支え続けてきた技術には、学ぶべき深い知見が詰まっています。

「怖くないですよ、一つずつ紐解けば簡単です」。この言葉を忘れずに、これからもPL/Iの奥深い世界を一緒に探求していきましょう!

それでは、また次回の記事でお会いしましょう!

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