【実務・中級編】ON条件におけるZERODIVIDEの捕捉とリカバリ処理 – PL/Iの基本構文とデータ制御実践ガイド

こんにちは。メインフレームの現場で、日々COBOLやPL/Iの山のようなソースコードと格闘している皆さん、お疲れ様です。

夜間バッチの真っ最中に、オペレータから「〇〇ジョブがABEND(異常終了)しました!」と連絡が入り、スプールを覗いたら `S0C7` や `S0C8`、あるいはPL/I特有のメッセージが――。冷や汗が流れる瞬間ですよね。

特に、長年稼働している基幹システムの勘定系や統計処理バッチにおいて、マスタデータのマスターメンテナンス漏れや、予期せぬゼロ除算(ZERODIVIDE)によるジョブの突然死は、運用担当者にとって悪夢のようなものです。「データが一部不正なんだから、そのレコードだけスキップするか、デフォルト値で埋めて最後まで走り抜けよ!」と、心の中でメインフレームに向かって叫んだ経験がある方も多いはずです。

今回は、PL/Iが誇る強力な例外処理機構である「ON条件(ON-unit)」、中でも`ZERODIVIDE`の捕捉とリカバリ処理について、実務でそのまま使える実装パターンを交えて徹底解説します。識別子の命名規則の自由度の高さゆえにカオスになりがちなPL/Iコードにおいて、堅牢なエラーハンドリングをどう組み込むか、私の経験を総動員して伝授しましょう。

1. PL/Iの「予約語レス」な世界とONユニットの思想

まず、PL/Iの言語仕様のユニークな点に触れておきます。C言語やJava、あるいはCOBOLのように「何でもかんでも予約語(Reserved Word)」にしてしまう言語とは異なり、PL/Iには厳密な意味での「予約語」が存在しません

例えば、`IF` や `READ`、さらには今回解説する `ZERODIVIDE` さえも、コンテキストキーワード(文脈依存語)として扱われます。そのため、変数名として `ZERODIVIDE` という名前を宣言してしまおうと思えばできてしまいます(絶対にやらないでくださいね!コンパイラが混乱し、後輩から絶望の目で見られます)。

この「自由度の高さ」の裏返しとして、PL/Iではシステムが予期せぬハードウェア例外(割り込み)を検知した際、プログラマが明示的に制御を奪うための割り込み処理機構(ON-unit)が言語仕様のコアとして組み込まれています。COBOLで言えば `USE AFTER EXCEPTION` や、デバッグ文を書き散らすアプローチが必要になるところを、PL/Iはスマートに、かつエレガントに記述できるのです。

2. ZERODIVIDE捕捉の制御フローと実務上の罠

ゼロ除算が発生した瞬間、ハードウェアレベルで割算例外(Fixed-Point or Floating-Point Divide Exception)が起きます。通常、これを放置すればOSが検知してジョブは強制終了(ABEND)です。

しかし、あらかじめプログラムのスコープ内で `ON ZERODIVIDE` を宣言しておくと、制御は即座にそのONユニット(無名ブロックまたはプロシージャ)へジャンプします。

ここでベテランとして皆さんに強く警告しておきたい実務上の罠があります。

1. 無限ループの恐怖:
ONユニット内で再度ゼロ除算が発生するような計算を書いてしまうと、永遠に処理が戻らず、CPU時間を食いつぶす無限ループ(またはスタックオーバーフロー)に陥ります。
2. 割算項目の特定:
どの変数とどの変数の演算でゼロ除算が起きたのか。ONユニット内だけでは、どのレコードの何行目で発生したかが直感的に分からない場合があります。そのため、発生時のキー項目(顧客IDや伝票番号など)をログに退避させる工夫が必須です。
3. GOTOによる脱出:
ONユニットの処理が終わった後、そのまま次の行に進むケースもありますが、ゼロ除算が起きた直後の演算式を再試行するわけにはいきません。基本的には `GOTO` ステートメントを使って、当該レコードの処理ループを抜け、次のレコード(VSAMの読み込みなど)へ強制的にジャンプさせるのが定石です。

3. 実践!VSAMファイル処理におけるZERODIVIDEリカバリ実装

それでは、実際のメインフレーム開発現場を想定したサンプルコードを見てみましょう。
月次売上データファイルを順編成(あるいはVSAM KSDSの順次アクセス)で読み込み、単価あたりの比率を計算するバッチプログラムの骨子です。分母(総数量)がゼロである不正データが含まれている想定で記述しています。

DIVCALC: PROC OPTIONS(MAIN);

/—————————————————————-/
/ 変数宣言部 /
/—————————————————————-/
DCL SALES_FILE FILE RECORD INPUT; / 入力ファイル /
DCL EOF_FLG CHAR(1) INIT(‘0’); / 終了フラグ /

/ レコード構造体 /
DCL 1 SALES_REC,
3 CUST_ID CHAR(5), / 顧客ID /
3 CUST_NAME CHAR(20), / 顧客名 /
3 TOTAL_SALES DEC FIXED(11,2), / 総売上金額 /
3 TOTAL_QTY DEC FIXED(7,0); / 総販売数量 /

/ 作業変数 /
DCL UNIT_PRICE DEC FIXED(9,2); / 単価(計算結果)/
DCL ERR_COUNT FIXED BIN(31) INIT(0); / エラーカウンタ /

/—————————————————————-/
/ ファイルオープン /
/—————————————————————-/
OPEN FILE(SALES_FILE);

/ 終了条件(ENDFILE)の定義 /
ON ENDFILE(SALES_FILE) EOF_FLG = ‘1’;

/—————————————————————-/
/ ★ここが今回のキモ:ZERODIVIDE例外の捕捉とリカバリ定義 /
/—————————————————————-/
ON ZERODIVIDE BEGIN;
ERR_COUNT = ERR_COUNT + 1;

/ 運用保守のためにSYSOUTへ警告を出力し、キーを特定できるようにする /
DISPLAY(‘【警告】ゼロ除算発生: 顧客ID = ‘ || CUST_ID);
DISPLAY(‘ 総売上=’ || TO_CHAR_FIXED(TOTAL_SALES) ||
‘ / 数量=’ || TO_CHAR_FIXED(TOTAL_QTY));

/ デフォルト値(今回はゼロ)をセットして計算結果破綻を防ぐ /
UNIT_PRICE = 0;

/ 演算エラーの行をスキップして、ループの抜け先(ラベル)へ強制ジャンプ /
GOTO NEXT_RECORD;
END;

/—————————————————————-/
/ メイン処理ループ /
/—————————————————————-/
READ FILE(SALES_FILE) INTO(SALES_REC);

DO WHILE (EOF_FLG = ‘0’);

/ 実際の割り算処理(ここでTOTAL_QTYが0の場合、自動的にONユニットへ飛ぶ) /
UNIT_PRICE = TOTAL_SALES / TOTAL_QTY;

/ 通常時の処理フロー(正常な単価を帳票や後続ファイルへ渡す処理など) /
CALL PROCESS_NORMAL_DATA();

NEXT_RECORD:
{
/ GOTOの着地点:次のレコードを読み込む /
read_next:
READ FILE(SALES_FILE) INTO(SALES_REC);
}

END;

/—————————————————————-/
/ 終了処理 /
/—————————————————————-/
CLOSE FILE(SALES_FILE);

DISPLAY(‘バッチ処理正常終了. ゼロ除算補正件数 = ‘ || LTRIM(CHAR(ERR_COUNT)));
RETURN;

/ 正常データ処理用内部プロシージャ(ダミー) /
PROCESS_NORMAL_DATA: PROC;
/ 通常のビジネスロジックがここに入る /
END PROCESS_NORMAL_DATA;

END DIVCALC;

4. コードの解説とアーキテクトからのアドバイス

上記のコードには、レガシーシステムを安全に延命・保守するためのノウハウが凝縮されています。

1. `ON ZERODIVIDE BEGIN; … END;` のブロック構造
単一のステートメントだけでなく、`BEGIN` ブロックで囲むことで、複数の処理(カウンタのインクリメント、コンソールへのメッセージ出力、デフォルト値の代入、ラベルへのジャンプ)を安全に一括実行できます。
2. `DISPLAY`ビルトイン関数による証跡の確保
基幹システムにおいて「何が起きたか分からない」のは最悪です。例外が起きた瞬間に、どの顧客IDで発生したのかを `DISPLAY` でジョブログ(JESMSGLG / JESYSPROK)に残すことで、後からデータ部の人間へ「このマスタ直してください」とエビデンス付きでエスカレーションできます。
3. `GOTO` による安全なエスケープ
前述した通り、例外発生箇所からそのままリターンすると無限ループの元になります。`NEXT_RECORD` ラベルへ飛ばすことで、不正なレコードの計算を安全に打ち切り、次のレコードの読込へスムーズに制御を戻すことができます。

まとめ

PL/Iの `ON ZERODIVIDE` をはじめとする例外処理機能は、正しく使えば「データ不備ごときで基幹バッチを止させない」という高い可用性(Resilience)を実現するための強力な武器になります。

「マスタが汚れているからプログラム側でカバーするのは邪道だ」という純粋主義的な意見もあるかもしれません。しかし、24時間365日止まることのない現代のインフラにおいて、夜間バッチの突発的なABENDがビジネスに与える損失は計り知れません。

古い仕様だからと敬遠せず、PL/Iの言語特性を深く理解し、手堅く安全な例外ハンドリングを実装していくこと――それこそが、今求められているメインフレーム・エンジニアの真価です。

皆さんの現場のバッチ処理が、今日も無事に定時終了することを祈っています!

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