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

メインフレームの深淵:PL/IのRECORD条件と極限の信頼性追求 – 基幹システムを守るI/Oエラーハンドリングの真髄

現代のシステムアーキテクチャはクラウドネイティブ、マイクロサービス、イベント駆動といった華やかなキーワードで彩られています。しかし、その根底には今もなお、人類が築き上げてきた情報システムの礎、すなわちメインフレームが脈々と稼働し続けています。その中でもIBMのPL/Iは、まさに基幹業務の神経線維とも言うべきI/O処理において、極限の信頼性を追求するための哲学を言語レベルで実装してきました。

JavaやC#といった現代言語へのシステム移行を検討されているテックリードやアーキテクトの皆さんにとって、PL/IのI/Oエラーハンドリング、特に`RECORD`条件の深い理解は、単なるレガシーコードの解析に留まらず、次世代システムが担保すべき「堅牢性」の真髄を学ぶ上で極めて重要な意味を持ちます。今回は、この`RECORD`条件に焦点を当て、その発生メカニズムからダンプ解析、さらには移行設計におけるエッジケースまで、メインフレームの深淵を覗いてみましょう。

PL/IにおけるRECORD条件とは何か、その発生メカニズム

PL/Iプログラムがファイルに対して入出力(I/O)を行う際、最も基本的なエラーの一つとして`RECORD`条件が挙げられます。これは、プログラム内で宣言されたレコードの構造(長さ)と、実際にファイルから読み書きしようとする物理レコードの長さが一致しない場合に発生する、まさに「型の不一致」に起因するエラーです。しかし、その発生理由は単なる長さの不一致に留まりません。

RECORD条件のトリガー

`RECORD`条件が発生する典型的なシナリオは以下の通りです。

1. レコード長不一致:

  • プログラム内の`DECLARE`文で定義されたレコード(構造体)の長さと、JCLの`DD`ステートメントや`ALLOCATE`コマンドで定義されたファイルのレコード長(`LRECL`)が異なる場合。
  • 可変長レコード(`V`、`VB`、`VBS`フォーマット)において、レコードの長さを示すRWD(Record Word Descriptor)が破損している、または不正な値になっている場合。
  • `READ FILE(…) INTO(…)` または `WRITE FILE(…) FROM(…)` の`INTO`句、`FROM`句で指定された変数の長さが、実際のレコード長と合致しない場合。

2. 物理I/Oエラー:

  • ディスク障害、テープドライブのヘッドエラー、ネットワークI/Oのタイムアウトなど、物理的な原因によりデータの読み書きが正常に完了できなかった場合。これは稀ではありますが、発生すると非常に深刻な事態を招きます。

3. 不適切なI/Oモード:

  • `STREAM`属性を持つファイルに対して`RECORD` I/Oを試みる、またはその逆の場合。
  • `INPUT`属性のファイルに`WRITE`しようとする、`OUTPUT`属性のファイルから`READ`しようとするなど、ファイル属性と操作が矛盾する場合。

PL/Iの`ON`ステートメントは、このような予期せぬ事態が発生した際に、プログラムの制御を特定の処理ブロック(`ON`ユニット)に移譲する強力なメカニズムです。`ON RECORD`は、まさにI/Oの整合性が崩れた際の最後の防壁として機能します。

コードで語る:RECORD条件の捕捉と診断の定石

基幹システムの現場では、`RECORD`条件が発生した際にいかに迅速かつ的確に状況を把握し、リカバリあるいは原因特定に繋げるかが問われます。以下に、`ON RECORD`ユニットの基本的な記述と、デバッグに役立つ組み込み関数を紹介します。

PLIREC_HANDLER: PACKAGE OPTIONS(MAIN);

/ 入力ファイル定義: 固定長レコード (LRECL=80) /
DCL INFILE FILE INPUT RECORD ENVIRONMENT(F(80));
/ 出力ファイル定義: 固定長レコード (LRECL=100) /
DCL OUTFILE FILE OUTPUT RECORD ENVIRONMENT(F(100));

/ 入力レコードの構造体定義 /
DCL 1 IN_REC,
2 IN_KEY CHAR(10),
2 IN_DATA1 FIXED DEC(7,2),
2 IN_DATA2 CHAR(20),
2 IN_FILLER CHAR(43); / 合計 10 + 4 + 20 + 43 = 77バイト (FIXED DECはパック10進数で内部表現されるため、ここでは7桁+2桁で4バイト消費と仮定) /
/ 実際のLRECL=80に対して、宣言上の長さが異なるためRECORD条件発生の可能性あり /

/ 出力レコードの構造体定義 /
DCL 1 OUT_REC,
2 OUT_ID CHAR(15),
2 OUT_VALUE FIXED DEC(9,2),
2 OUT_COMMENT CHAR(70); / 合計 15 + 5 + 70 = 90バイト /
/ 実際のLRECL=100に対して、宣言上の長さが異なるためRECORD条件発生の可能性あり /

/ 可変長レコード処理のためのポインタとベース変数 /
DCL REC_PTR POINTER;
DCL 1 VAR_REC BASED(REC_PTR),
2 VAR_LEN BIN FIXED(15), / 可変長レコードの長さ情報 (RWDの2バイト目に相当) /
2 VAR_DATA CHAR(256) VARYING; / 可変長データ部分 /

/ RECORD条件のONユニット /
ON RECORD BEGIN;
PUT FILE(SYSPRINT) SKIP LIST(‘ PL/I RECORD条件 発生! ‘);
PUT FILE(SYSPRINT) SKIP LIST(‘ ONCODE = ‘ || ONCODE()); / 発生した条件コード /
PUT FILE(SYSPRINT) SKIP LIST(‘ ONFILE = ‘ || ONFILE()); / エラーが発生したファイル名 /
PUT FILE(SYSPRINT) SKIP LIST(‘ ONKEY = ‘ || ONKEY()); / キー付きファイルの場合のキー値 /
PUT FILE(SYSPRINT) SKIP LIST(‘ ONSOURCE = ‘ || ONSOURCE()); / 問題のレコードの内容 (入力時) /
PUT FILE(SYSPRINT) SKIP LIST(‘ ONLOC = ‘ || ONLOC()); / エラー発生箇所のPROCEDURE名 /

/ エラー発生時の変数ダンプ (デバッグに非常に有効) /
PUT FILE(SYSPRINT) SKIP LIST(‘— 変数ダンプ —‘);
PUT FILE(SYSPRINT) SKIP DATA(IN_REC, OUT_REC); / 現在のレコード内容を出力 /
IF REC_PTR ^= NULL THEN DO;
PUT FILE(SYSPRINT) SKIP LIST(‘ VAR_REC (BASED by REC_PTR) at ADDRESS: ‘ || ADDR(VAR_REC));
PUT FILE(SYSPRINT) SKIP DATA(VAR_REC.VAR_LEN, VAR_REC.VAR_DATA);
END;

/ PL/Iの診断情報を出力し、ABENDダンプを生成 /
CALL PLIDUMP; / IBM提供の診断ルーチン。SYSUDUMP/SYSDUMPに詳細な情報を出力 /

/ 致命的なエラーとしてプログラムを終了させるか、エラーレコードをスキップして継続するか /
/ 一般的には、基幹システムでは即座にABENDさせて調査を優先します /
STOP; / プログラムを強制終了 (ABEND S000) /
/ または GO TO ERROR_RECOVERY; // エラーリカバリ処理へジャンプ /
END;

/ ファイル終了条件のONユニット /
ON ENDFILE(INFILE) BEGIN;
PUT FILE(SYSPRINT) SKIP LIST(‘ 入力ファイル処理終了 ‘);
GOTO FILE_END; / ファイル終了後の処理へジャンプ /
END;

PLIPROC: PROCEDURE OPTIONS(MAIN);

OPEN FILE(INFILE);
OPEN FILE(OUTFILE);

DO WHILE(‘1’B); / 無限ループ (ENDFILE条件で脱出) /
/ 例1: 固定長レコードの読み込み /
/ LRECL=80のINFILEから、IN_REC (宣言長77バイト) にREADするとRECORD条件が発生し得る /
READ FILE(INFILE) INTO(IN_REC);

/ ここに正常な業務ロジックを記述 /
OUT_REC.OUT_ID = IN_REC.IN_KEY || ‘PROCESSED’;
OUT_REC.OUT_VALUE = IN_REC.IN_DATA1 1.05; / 5%増し /
OUT_REC.OUT_COMMENT = IN_REC.IN_DATA2 || ‘ from INFILE’;

/ 例2: 固定長レコードの書き込み /
/ LRECL=100のOUTFILEに、OUT_REC (宣言長90バイト) からWRITEするとRECORD条件が発生し得る /
WRITE FILE(OUTFILE) FROM(OUT_REC);
END;

FILE_END:; / ファイル処理終了後のラベル /
CLOSE FILE(INFILE);
CLOSE FILE(OUTFILE);

END PLIPROC;

END PLIREC_HANDLER;

上記のコード例では、`ON RECORD`ユニット内で`ONCODE`、`ONFILE`、`ONKEY`、`ONSOURCE`、`ONLOC`といった組み込み関数を活用し、エラー発生時の詳細な状況を`SYSPRINT`に出力しています。特に`ONSOURCE()`は、入力操作で`RECORD`条件が発生した場合に、実際に読み込まれた生のレコード内容を文字列として返してくれるため、データの破損状況を把握する上で非常に強力な手がかりとなります。

そして、最も重要なのが`CALL PLIDUMP;`です。これはIBMが提供する診断ルーチンで、呼び出すと現在のプログラムの状態(レジスタ、スタック、メモリの内容など)を`SYSUDUMP`または`SYSDUMP`に書き出します。基幹システムでは、`RECORD`条件のような致命的なI/Oエラーが発生した場合、多くは即座に`PLIDUMP`を呼び出してプログラムを`STOP`(ABEND)させ、詳細なダンプ解析によって原因を特定する運用がなされます。

実践例:ベース変数とポインタによる可変長レコード処理

PL/Iの真骨頂は、ポインタ(`POINTER`)とベース変数(`BASED`)を駆使した動的なメモリ操作にあります。これは可変長レコード(`V`、`VB`、`VBS`フォーマット)を扱う際に特に威力を発揮します。

/ 可変長レコードファイル定義 (RECFM=VB, LRECL=260) /
DCL VAR_FILE FILE INPUT RECORD ENVIRONMENT(V(260));

/ 可変長レコードを指すポインタ /
DCL VAR_REC_PTR POINTER;

/ ポインタで指し示す領域のレイアウト /
DCL 1 VAR_REC_LAYOUT BASED(VAR_REC_PTR),
2 RDW BIN FIXED(31), / レコード記述語 (Record Descriptor Word) /
2 DATA_LENGTH BIN FIXED(15) DEF RDW POS(1), / RDWの上位2バイトはレコード長 /
2 FILLER_RDW BIN FIXED(15) DEF RDW POS(3), / RDWの下位2バイトは未使用 /
2 RECORD_DATA CHAR(256); / 実際のデータ部分 (最長256バイトと仮定) /

ON RECORD BEGIN;
/ … (前述のONユニットと同様のエラー出力とPLIDUMP) … /
PUT FILE(SYSPRINT) SKIP LIST(‘ VAR_REC_PTR = ‘ || HEX(VAR_REC_PTR)); / ポインタの値 /
IF VAR_REC_PTR ^= NULL THEN DO;
PUT FILE(SYSPRINT) SKIP DATA(VAR_REC_LAYOUT.RDW, VAR_REC_LAYOUT.DATA_LENGTH, VAR_REC_LAYOUT.RECORD_DATA);
END;
STOP;
END;

/ … (PLIPROC内で) … /
OPEN FILE(VAR_FILE);
DO WHILE(‘1’B);
/ SET(VAR_REC_PTR) を使うことで、ファイルバッファ上のレコードに直接ポインタをセット /
/ INTO句ではなくSET句を使用するため、データコピーが発生しない /
READ FILE(VAR_FILE) SET(VAR_REC_PTR);

/ RDWから実際のデータ長を取得し、それに応じて処理 /
DCL ACTUAL_DATA_LEN BIN FIXED(15);
ACTUAL_DATA_LEN = VAR_REC_LAYOUT.DATA_LENGTH – 4; / RDWの4バイトを引く /

PUT FILE(SYSPRINT) SKIP LIST(‘ Read record at address: ‘ || HEX(VAR_REC_PTR));
PUT FILE(SYSPRINT) SKIP LIST(‘ RDW: ‘ || HEX(VAR_REC_LAYOUT.RDW) || ‘, Data Length: ‘ || ACTUAL_DATA_LEN);
PUT FILE(SYSPRINT) SKIP LIST(‘ Data: ‘ || SUBSTR(VAR_REC_LAYOUT.RECORD_DATA, 1, ACTUAL_DATA_LEN));

/ … 業務ロジック … /
END;
CLOSE FILE(VAR_FILE);
/ … /

`READ FILE(…) SET(VAR_REC_PTR)`は、レコードをプログラム変数にコピーするのではなく、ファイル入力バッファ内のレコードの先頭アドレスを`VAR_REC_PTR`にセットします。これにより、データコピーのオーバーヘッドを削減し、非常に効率的なI/O処理が可能になります。

この`SET`モードI/Oで`RECORD`条件が発生した場合、`VAR_REC_PTR`が指すアドレスが不正であったり、RWDが破損していたりする可能性が高いです。ダンプ解析では、`VAR_REC_PTR`の値と、それが指し示すメモリ領域の内容を徹底的に確認する必要があります。

アベンド(ABEND)発生時のダンプ解析術

メインフレームの真髄は、ABEND発生時のダンプ解析にあります。特にPL/Iの場合、プログラムの実行を追跡し、メモリ上のデータ状態を把握することで、`RECORD`条件の根本原因を特定することができます。

1. SYSUDUMP/SYSDUMPの読み方:

  • ダンプの冒頭には、ABENDコード(S0C4, S0C7, S0CBなど)とPSW(Program Status Word)が表示されます。PSWの下位バイトには、ABEND発生時の命令アドレスが含まれており、ここからエラー発生箇所を特定します。
  • PSW: アベンド時のPSWから実行されていた命令アドレス(Instruction Address)を確認します。このアドレスが、PL/Iコンパイラリストのオフセット情報と照合することで、どのPL/Iステートメントでエラーが発生したかを特定できます。
  • レジスタ: R0〜R15の汎用レジスタの値を確認します。特に、R13はセーブエリアチェーンの起点となり、呼び出し元のレジスタ内容を辿ることで、関数呼び出しの階層と各関数の状態を把握できます。R14はリターンアドレス、R15は呼び出し先のエントリーポイントを指すことが多いです。
  • セーブエリアチェーン: R13が指すセーブエリアを辿ることで、どのプロシージャがどのプロシージャを呼び出したか、その際のレジスタ値がどうだったかを遡ることができます。これにより、I/O処理に至るまでのコールスタックを把握できます。

2. PL/Iプログラムにおけるオフセットとソース行のマッピング:

  • コンパイラオプション`OFFSET`を指定してコンパイルされたリストには、生成された機械語コードのオフセットアドレスと、対応するPL/Iソースコードの行番号が記載されています。PSWの命令アドレスをこのリストと照合することで、エラー発生源のPL/I文をピンポイントで特定できます。
  • `RECORD`条件の場合、多くは`READ`または`WRITE`文で発生しているはずです。

3. ポインタやBASED変数のアドレス値から、メモリ上の実データを探る:

  • ダンプ内のポインタ変数(`REC_PTR`など)のアドレス値を確認し、そのアドレスが指すメモリ領域の内容をダンプ内で探します。
  • これにより、`RECORD`条件発生時に実際にメモリ上に存在していたレコードの内容や、可変長レコードのRWDがどのような状態だったかを詳細に分析できます。もしRWDが不正であれば、上流の処理でデータが破壊された可能性が浮上します。

深掘り:パックデシマルの内部符号反転バグとI/Oの闇

`RECORD`条件そのものとは少し趣が異なりますが、I/O処理後のデータ整合性に関わる、メインフレームならではの深刻なバグとして、パックデシマル(`FIXED DECIMAL`)の内部符号反転バグが存在します。

PL/Iの`FIXED DECIMAL`型は、COBOLの`COMP-3`と同様に、内部的にはパック10進形式で格納されます。これは、1バイトに2桁の10進数を格納し、最下位バイトの下位4ビットに符号(`C`=正、`D`=負、`F`=符号なし)を持つ形式です。

例: +12345 (FIXED DEC(5)) -> X’12345C’
例: -12345 (FIXED DEC(5)) -> X’12345D’

この符号が、何らかの理由で不正な値(例えば`X’F’`以外のEBCDIC文字のゾーン部など)に上書きされてしまうと、一見正常な数値に見えても、その後の算術演算で`SIZE`条件や`FIXEDOVERFLOW`条件、あるいはS0C7(データ例外)ABENDを引き起こすことがあります。

発生シナリオの例:

  • 外部ファイルから読み込んだEBCDIC形式の数値データが、適切な変換ルーチンを通さずに、誤って直接`FIXED DECIMAL`フィールドにMOVEされた場合。例えば、`F1F2F3F4F5`(EBCDICの”12345″)がパック10進フィールドに上書きされると、内部では`12345F`のように解釈される可能性があります。
  • CICSのCOMMAREAなど、共有メモリ領域でデータを受け渡す際に、データの長さやオフセットを誤り、隣接する領域のデータが`FIXED DECIMAL`フィールドの符号部を破壊してしまうケース。
  • アセンブラルーチンやC言語ルーチンとの連携で、バイト境界やデータ型アライメントの認識を誤り、メモリ破壊が発生するケース。

`RECORD`条件は「長さ」の不一致ですが、この符号反転バグは「内容」の破壊であり、I/Oバッファ内で発生した不正なデータが、後続の処理で致命的な結果を招く典型例です。ダンプ解析では、`FIXED DECIMAL`変数の内容を16進数で確認し、最下位バイトの下位4ビットが`C`、`D`、`F`のいずれかであることを常に意識する必要があります。

コンパイラオプションと最適化の功罪

PL/Iコンパイラには、デバッグ支援や実行時性能に影響を与える様々なオプションがあります。これらを理解しておくことは、`RECORD`条件のデバッグや、移行時の品質確保に直結します。

  • `CHECK`オプション:

`CHECK(SIZE, FIXEDOVERFLOW, SUBSCRIPTRANGE, STRINGRANGE, ZERODIVIDE)`などを指定することで、実行時にこれらの条件が発生した際に`ON`ユニットが起動するようになります。デバッグ時には非常に有用ですが、本番環境ではオーバーヘッドが大きいため、通常は外されます。しかし、前述のパックデシマル符号反転バグのような潜在的なデータ破壊を検出するには有効な手段となり得ます。

  • `OPTIMIZE`レベル:

`OPTIMIZE(0)`から`OPTIMIZE(2)`まであり、高いレベルほど生成されるコードが効率的になります。しかし、最適化レベルが高いと、ソースコードと機械語コードのマッピングが複雑になり、ダンプ解析時のソース行特定が難しくなることがあります。デバッグやダンプ解析の頻度が高いシステムでは、`OPTIMIZE(0)`や`OPTIMIZE(1)`に留めることも検討されます。

  • `OFFSET`、`LINECOUNT`、`XREF`:
  • `OFFSET`: コンパイルリストに機械語コードのオフセットアドレスを出力し、ダンプ解析を容易にします。これは必須級のオプションです。
  • `LINECOUNT`: ソース行番号と生成されるコードの対応を明確にします。
  • `XREF`: 変数やラベルの参照箇所を一覧表示し、コードの依存関係を把握するのに役立ちます。

これらのコンパイラオプションの選択は、システムの信頼性とデバッグ容易性のバランスをどう取るかという、アーキテクトの思想が色濃く反映される部分です。

システム移行の視点:PL/IのI/O堅牢性を現代にどう繋ぐか

PL/Iの`RECORD`条件ハンドリングや、それに付随するダンプ解析といった低レベルなデバッグ手法は、JavaやC#などの現代言語への移行において、大きな課題となります。

データ型マッピングとEBCDIC/ASCII変換の課題

  • `FIXED DECIMAL`の再現性: PL/Iの`FIXED DECIMAL`は、多くの言語ではそのまま再現できません。Javaでは`BigDecimal`、C#では`decimal`型が最も近いですが、厳密なバイト表現や桁数管理、そして何よりもパック10進数の概念が異なります。移行時には、数値演算の精度誤差や、前述のような内部符号反転バグの再発防止策を徹底的に設計する必要があります。
  • EBCDIC/ASCII変換: メインフレームのデータはEBCDICですが、オープン系のシステムはASCIIです。ファイルI/Oの際には必ずこの変換が発生し、特に文字コードセットが混在する場合(例:日本語EBCDICと英数字EBCDIC)や、バイナリデータと文字データが混在するレコードの場合、予期せぬ文字化けやデータ破損を引き起こす可能性があります。`RECORD`条件は長さの不一致で発生しますが、変換後のデータ長や内容の整合性も同時に担保しなければなりません。

PL/Iの動的・低レベルエラーハンドリングの再現性

  • `ON`ユニットの代替: PL/Iの`ON`ユニットは、実行時に動的にエラーハンドリングロジックを切り替える強力な機能です。Javaの`try-catch`ブロックやC#の`try-finally`ブロックは似ていますが、PL/Iのように特定の条件が発生した際に「割り込み」として処理を移譲するモデルとは異なります。移行先では、I/Oエラーを捕捉するためのカスタム例外クラスや、統一されたエラーロギング・リカバリフレームワークの構築が不可欠です。
  • ポインタとBASED変数の再現: PL/IのポインタとBASED変数による動的メモリ操作は、C言語のそれと近い概念です。JavaやC#にはポインタは存在せず、オブジェクト参照とガベージコレクションによるメモリ管理が主流です。したがって、PL/Iでポインタを多用しているコードは、移行時にオブジェクト指向の設計原則に基づき、クラスや構造体、コレクションに置き換える必要があります。この際、メモリの厳密なバイト単位の操作が失われるため、レコード長やオフセットの不一致は、より抽象的なレベルでのデータ不整合として現れることになります。

CICSオンライン処理とDB2連携のエッジケース

  • CICSにおけるI/Oエラー: CICSアプリケーションにおけるファイルI/Oは、通常`EXEC CICS READ FILE(…) INTO(…)`のようなCICSコマンドを介して行われます。この場合、ファイルI/Oエラーは`RESP`コードや`RESP2`コードとして返されるため、PL/Iの`ON RECORD`条件は通常発生しません。CICSシステムがI/Oエラーを捕捉し、プログラムにはCICS独自の仕組みで通知します。しかし、非常に稀なケースとして、CICSプログラムが`FILE`属性を持つ`DD`参照を直接`OPEN`して`READ/WRITE`を行うような非定型処理を実装している場合、`RECORD`条件が発生する可能性はゼロではありません。移行時には、このようなアンチパターンが隠れていないか、徹底的にコードレビューを行う必要があります。
  • DB2におけるエラーハンドリング: DB2データベースへのアクセスは、埋め込みSQL(`EXEC SQL …`)を介して行われます。この際のエラーは`SQLCA`(SQL Communication Area)構造体内の`SQLCODE`や`SQLSTATE`でチェックするのが定石であり、`RECORD`条件は直接的には適用されません。ホスト変数とDB2テーブルのカラム定義の不一致は、`SQLCODE`エラーとして検出されます。ただし、DB2からLOB(Large Object)データを取り出し、それをOSファイルに書き出すような処理においては、PL/IのファイルI/Oが関与するため、この部分で`RECORD`条件が発生する可能性はあります。

結論

PL/Iの`RECORD`条件は、単なるI/Oエラーハンドリングの一環ではありません。それは、メインフレーム上で稼働する基幹システムが、いかにデータの完全性とシステムの堅牢性を追求してきたかの象徴であり、その設計思想の根幹をなすものです。

`ON RECORD`ユニットによるエラー捕捉、`ONCODE`や`ONSOURCE`による詳細な状況把握、そして`PLIDUMP`からのダンプ解析という一連のプロセスは、現代のシステムにおいても通用する「低レベルなメカニズムへの深い洞察」の重要性を示唆しています。

JavaやC#といったオープン系プラットフォームへの移行を担うシステムアーキテクトの皆さんにとって、PL/Iの`RECORD`条件がもたらす意味合いを深く理解することは、単なるレガシーコードの「翻訳」に留まらず、次世代システムに「基幹の魂」を吹き込み、極限の信頼性を設計するための不可欠な知見となるでしょう。メインフレームが培ってきた堅牢性の哲学を理解し、それを現代のアーキテクチャにどう昇華させるか、その問いへの答えは、まさにこの`RECORD`条件の深淵に隠されています。

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