メインフレームの現場から:ゼロ除算という「時限爆弾」にどう立ち向かうか
おい、調子はどうだ?
今日もどこかのジョブネットで、膨大な数の勘定系マスターレコードが猛烈なスピードで処理されていることだろう。
さて、君は最近、夜間バッチの終了コード「U4038」や「SCC0」に冷や汗をかかされたことはないか?
原因を突き詰めていくと、大抵はマスタデータの汚れ、例えば「数量」や「単価」といった数値項目に想定外のゼロ(或者は空白パディングによる暗黙のゼロ)が混入しており、それを何のガードもなしに割算(算術除算)に使ってしまったがために発生するゼロ除算例外(ZERODIVIDE)に行き着く。
COBOLであれば `ON SIZE ERROR` やコンパイラオプションの `ARITH(EXTEND)` で何とかなる話かもしれないが、我々が扱う PL/I(Programming Language One) の世界では、こうした例外処理はもっとエレガントかつ強力に、そして時として圧倒的にシビアに制御する必要がある。
今回は、PL/Iにおける `ON ZERODIVIDE` を使った割り込みトラップと、実務のVSAMファイル更新バッチで絶対に押さえておかなべきリカバリの極意を伝授しよう。
—
1. PL/Iの予約語神話と「ONユニット」の正体
まず、PL/Iの非常にユニークな言語特性についておさらいしておこう。
C言語やJavaとは異なり、PL/Iには厳密な意味での「予約語(Reserved Words)」が存在しない。
どういうことか? `IF` や `DO` といったキーワードであっても、文脈上それが変数名であると判別できるならば、識別子として使うことが理論上できてしまう(コンパイラが泣くので絶対にやらないがな)。
この「コンテキスト依存の柔軟性」は、例外処理機構である ON条件(On-conditions) にも色濃く反映されている。
`ZERODIVIDE` は組み込みの条件名だが、これも単なる予約されたキーワードというよりは、言語処理系があらかじめ定義しているシステム条件の一つに過ぎない。
ONユニットの制御フロー
PL/Iの `ON` 文は、いわゆる他言語の `try-catch` ブロックとは少し毛色が違う。
これは「割り込みイベント駆動型のトラップ定義」だ。
1
ON ZERODIVIDE BEGIN;
/ ゼロ除算が発生した瞬間にここへジャンプする /
END;
プログラムの実行中にゼロ除算(例えば固定小数点数や浮動小数点数の割算で分母が `0` になった場合)が発生すると、ハードウェアの割り込みが検知され、PL/Iランタイム環境(LE: Language Environment)が制御を奪う。そして、直近で有効化されている `ON ZERODIVIDE` のONユニットへ制御が移る仕組みだ。
ここで重要なのは、「リカバリ後の制御戻り先」である。
例外処理を行ったあと、エラーとなった演算の「次の文」に進むのか、それとも異常終了させるのか。ここをコントロールするのが、PL/I特有の `GOTO` や暗黙の制御フローの理解なのだ。
—
2. 実務で使える!VSAMマスター更新バッチの堅牢なコード例
百聞は一見にしかずだ。
日々の月次売上計算バッチを想定してくれ。VSAM(KSDS)から顧客ごとの累計売上と取引回数を読み込み、平均単価を算出するプログラムだ。もし取引回数が「0」だった場合、当然ゼロ除算が発生する。これをクラッシュさせずにデフォルト値(例えば `0`)で安全にバイパスし、ログに警告を出力して処理を継続する実用的なコードを書いた。
大文字ベース、適切なインデント、そしてPL/Iの神髄であるBUILTIN関数(`ONCODE` や `ONSOURCE` など)をフル活用した、現場の基準に耐えうるコードだ。
1
CALC_AVG: PROC OPTIONS(MAIN);
/————————————————————–/
/ ファイル定義 (VSAM KSDS) /
/————————————————————–/
DCL CUST_MASTER FILE RECORD
ENV(VSAM);
/————————————————————–/
/ レコード構造体定義 /
/————————————————————–/
DCL 1 CUST_REC,
5 CUST_ID CHAR(5), / 顧客ID /
5 CUST_NAME CHAR(30), / 顧客名 /
5 TOTAL_SALES DEC FIXED(11,2), / 累計売上金額 /
5 TRANS_COUNT DEC FIXED(5,0), / 取引回数 /
5 AVG_PRICE DEC FIXED(9,2); / 平均単価 (演算結果) /
/————————————————————–/
/ ワーク変数・フラグ定義 /
/————————————————————–/
DCL EOF_FLAG CHAR(1) INIT(‘OFF’);
DCL ERROR_CNT FIXED BIN(31) INIT(0);
/————————————————————–/
/ ファイルオープン /
/————————————————————–/
OPEN FILE(CUST_MASTER) UPDATE;
/————————————————————–/
/ ON ZERODIVIDE ユニットの定義 (エントリ・スコープ) /
/————————————————————–/
ON ZERODIVIDE
BEGIN;
/ ゼロ除算発生時のリカバリ処理 /
ERROR_CNT = ERROR_CNT + 1;
PUT SKIP EDIT (‘[WARNING] ゼロ除算検出: 顧客ID = ‘, CUST_ID,
‘ / 取引回数がゼロのため平均単価を 0 に設定します。’)
(A, A, A);
/ ゼロ除算が発生した演算項目の結果に安全なデフォルト値を代入 /
AVG_PRICE = 0;
/
- 重要: GOTOを書かない場合、ONユニットを抜けた後に
- 例外が発生した文の「次」の文から実行が再開される。
/
END;
/————————————————————–/
/ メイン処理ループ /
/————————————————————–/
DO WHILE (EOF_FLAG = ‘OFF’);
READ FILE(CUST_MASTER) INTO(CUST_REC);
IF EOF_FLAG = ‘ON’ THEN
LEAVE;
/ — ここでゼロ除算の危険性がある演算 — /
/ TRANS_COUNT が 0 の場合、ハードウェア割り込みが発生し、 /
/ 上記の ON ZERODIVIDE ユニットが即座に捕捉する。 /
AVG_PRICE = TOTAL_SALES / TRANS_COUNT;
/ 計算結果を反映してVSAMレコードを書き戻す /
REWRITE FILE(CUST_MASTER) FROM(CUST_REC);
END;
/————————————————————–/
/ 終了処理 /
/————————————————————–/
CLOSE FILE(CUST_MASTER);
PUT SKIP EDIT (‘正常終了: 処理完了. ゼロ除算補正件数 = ‘, ERROR_CNT) (A, F(5));
RETURN;
/ 読み込み終了(ENDFILE)時の処理 /
ON ENDFILE(CUST_MASTER)
EOF_FLAG = ‘ON’;
END CALC_AVG;
—
3. シニアアーキテクトが教える「デバッグとコーディングの勘所」
このコードを眺めて、「なるほど、これで完璧だな」と思ったそこの君。
もう少し実務の泥臭い話をしよう。メインフレームの現場で生き残るためには、コードの表面だけでなく、コンパイラの挙動やOSのランタイム(Language Environment)の仕様まで見通す目が必要だ。
① ONユニットの「スコープ(有効範囲)」に気をつけろ
PL/Iの `ON` 条件は動的なスコープを持つ。つまり、どのブロック(PROCやBEGIN-END)の内部で `ON` を宣言したかによって、その有効範囲が変わる。
もしサブルーチンやネストしたブロック内で `ON ZERODIVIDE` を再定義し忘れると、予期せぬ上位ブロックのトラップが発動したり、最悪の場合はデフォルトのシステムアクション(U4038異常終了)に落ちることになる。例外トラップは、できるだけメインルーチン、あるいは影響範囲を限定した適切なブロックの階層で定義し、スコープの迷子にならないように徹底すること。
② 無限ループの罠に注意しろ
もし `ON ZERODIVIDE` の中で、分母を修正せずに同じ演算を再実行するようなコード(例えば `GOTO` で演算文に戻るなど)を書いてしまったらどうなるか?
そう、無限ループ(あるいは例外のネスト爆発)だ。
上記のサンプルコードでは、`ON` ユニット内で `AVG_PRICE = 0;` と直接結果変数に代入し、`GOTO` を使わずにユニットを抜けている。これにより、PL/Iランタイムは「例外の起きた演算の代入先にはすでに値が入った」とみなし、安全に次の行(`REWRITE`)へ進んでくれる。これが最も安全でスマートな作法だ。
③ ログ出力を怠るな
「プログラムを落とさない」ことはバッチの継続性において重要だが、「データ汚染を見逃す」ことは金融・基幹システムにおいて最大の罪だ。
ゼロ除算が発生したということは、マスタの前提データが崩れているか、前工程の集計バッチにバグがある証拠。必ず `PUT SKIP` やシステムログ(LEのメッセージサービス等)に、該当のキー(上記の例では `CUST_ID`)を出力させ、運用監視チームや保守担当者が翌朝の点検で即座に気づける仕組みをセットで実装しておけ。
—
おわりに
PL/Iは、古い言語だのレガシーだのと揶揄されることもあるが、システムズ・プログラミングから巨大な商用トランザクション処理までこなす、極めて洗練された怪物のような言語だ。特に今回紹介したような例外処理機構(`ON条件`)の精緻さは、現代の言語に見劣りするどころか、堅牢性が求められる基幹系において今なお強力な武器となる。
「なぜエラーになるのか」を言語の仕様レベルで理解し、「どうリカバリすべきか」をアーキテクチャの観点からデザインする。
それこそが、我々メインフレームエンジニアのプライドというものだ。
次のバッチ改修では、ただ動くだけのコードではなく、こうした例外制御まで完璧に織り込んだ「美しいコード」を書いてみせてくれ。期待しているぞ。
