おい、調子はどうだ?
今夜も深夜バッチのRC(リターンコード)4や8に頭を悩ませていないか?
「おい、また夜間バッチが落ちてるぞ!原因はなんだ!?」
――運用監視オペレータからの焦った内線電話。こういう修羅場、メインフレームの保守現場じゃ日常茶飯事だよな。
今夜は、我々が日々向き合っている基幹システムの心臓部、PL/Iで書かれたバッチプログラムにおいて、最も古典的でありながら、最も厄介なバグの一つである「ZERODIVIDE条件」について、徹底的に解説してやろう。
マニュアルの仕様をただなぞるだけの退屈な話はしない。VSAMファイルの読み込み、不意に混入するゼロ、ONユニットによる制御、そしてシステムダンプから一瞬で原因を特定するプロの技まで、現場で本当に役立つノウハウを叩き込んでやる。心してついて来い。
—
1. ZERODIVIDE条件の発生メカニズムと現場のリアル
固定小数点数(`FIXED BINARY` および `FIXED DECIMAL`)を使った除算において、除数(割る数)が「0」になった瞬間、硬件(ハードウェア)の割込みが走り、PL/Iのランタイム環境は例外を検知する。これが ZERODIVIDE条件 だ。
商 = 被除数 ÷ 除数 ← ここが「0」になった瞬間にゲームオーバー
「そんなの、割る前に `IF DIVISOR = 0` で弾けばいいだろ」と思ったそこの君。甘い。
金融機関や流通系の巨大な勘定系バッチシステムを舐めてもらっては困る。VSAMファイルから読み込んだマスタデータの数値項目が、前工程のバグやデータ移行(マイグレーション)の不手際で、全角スペースやパックスペース、あるいは予期せぬ「LOW-VALUES」で汚染されていることは珍しくない。
特に `FIXED DECIMAL`(ゾーン10進数やパック10進数)を `PIC` 句なしで直接 `FIXED BINARY` や他の数値に暗黙の型変換を伴って演算させた場合、コンパイラは忠実にコードを生成するが、実行時にデータ異常で除数が「0」とみなされ、容赦なくabend(異常終了)を引き起こす。
現場でよくある最悪のシナリオはこうだ。
1. 夜間バッチが深夜2時に `S0C7` ではなく、PL/Iのランタイムメッセージ(IBM0231Sなど)を吐いて異常終了。
2. 担当者が叩き起こされ、SYSUDUMPを調査。
3. どこの何行目でゼロ除算が起きたのか、最適化(OPTIMIZE)されたコードの中で迷子になる。
この悪夢を回避するための技術的アプローチを次から見ていこう。
—
2. 標準的な言語仕様とONユニットによる防御的プログラミング
PL/Iには、例外が発生した際にプログラムが即死するのを防ぎ、自前のリカバリールーチンに処理を逃がすための ONユニット(ON-Unit) という強力な機構が備わっている。
もし、外部ファイルからの入力値に「ゼロ」が混入するリスクがどうしても拭えない場合、あるいは予期せぬ例外に対してバッチを安全にトレイリング(エラーログを出して正常終了または安全なリターンコードで終了)させたい場合は、`ON ZERODIVIDE` を仕掛けるのがプロの作法だ。
だが、安易な `ON-unit` の乱用は禁物だ。「とりあえずONユニットでトラップしておけば落ちないや」なんて甘い考えでいると、根本的なデータ汚染の原因を見落とし、後続の処理でさらなる大惨事(データベースの整合性破壊など)を引き起こす。ONユニットは、あくまで「最後の安全弁」として使うべきだ。
—
3. 実践:VSAM入力とZERODIVIDEを制御するPL/Iプログラム
百聞は一見に如かずだ。
実際に、VSAMのKSDS(キー順データセット)から単価と数量を読み込み、売上金額を計算するバッチプログラムのサンプルコードを見せよう。
大文字ベースの記述、適切なインデント、そして組み込み関数(BUILTIN)を駆使した、そのまま現場で使える実践的なコードだ。
1
——————————————————————;
- プログラム名: SLSCAL01
- 概要: VSAMファイルを読み込み、ゼロ除算をガードしながら売上単価を算出
——————————————————————;
SLSCAL01: PROC OPTIONS(MAIN);
DCL 1 CUST_REC,
10 CUST_ID PIC ‘(5)9’, 顧客ID
10 SALES_QTY FIXED BIN(31,0), 販売数量
10 TOTAL_REV FIXED DEC(11,2); 総売上高
DCL UNIT_PRICE FIXED DEC(9,2); 算出した単価
DCL IO_ERR_FLG BIT(1) INIT(‘0’B); 入出力エラーフラグ
- VSAMファイル(KSDS)の宣言
DCL CUSTFILE FILE RECORD INPUT
ENVIRONMENT(BUFND(4) BUFNI(2));
- 異常終了を回避するためのONユニット(ZERODIVIDEトラップ)
ON ZERODIVIDE BEGIN;
DISPLAY(‘ WARNING: ZERODIVIDE DETECTED IN CUST_ID = ‘ || CUST_ID);
DISPLAY(‘ 数量がゼロまたは不正なため、単価をゼロとして処理を続行します。’);
UNIT_PRICE = 0;
- ゴトシ(GOTO)で例外発生箇所の次の処理へ脱出
GOTO CALC_END;
END;
- ファイルオープン
OPEN FILE(CUSTFILE);
- 読み込みループ
DO FOREVER;
READ FILE(CUSTFILE) INTO(CUST_REC);
IF IO_ERR_FLG THEN LEAVE;
- デバッグ用ビルトイン関数SUBSTRやFIXEDの活用例
- ここで SALES_QTY が 0 の場合、ハードウェア例外が発生しONユニットへジャンプする
IF SALES_QTY > 0 THEN DO;
UNIT_PRICE = TOTAL_REV / FIXED(SALES_QTY, 9, 2);
;
ELSE DO;
- 明示的なゼロチェックによる事前防御
UNIT_PRICE = 0;
DISPLAY(‘INFO: 数量ゼロを事前検知スキップ. ID=’ || CUST_ID);
END;
CALC_END:;
- ここで算出した UNIT_PRICE を使った後続処理が続く
CALL WRITE_PROCESS(CUST_ID, UNIT_PRICE);
END;
- ファイルクローズ
CLOSE FILE(CUSTFILE);
RETURN;
——————————————————————;
- 内部サブルーチン: 結果出力モジュール呼出
——————————————————————;
WRITE_PROCESS: PROC(P_ID, P_PRICE);
DCL P_ID PIC ‘(5)9’;
DCL P_PRICE FIXED DEC(9,2);
- 実際の業務ではここでDB2や出力用ファイルへ書き込む
END WRITE_PROCESS;
END SLSCAL01;
このコードのポイントを解説しよう。
1. `ON ZERODIVIDE` のスコープ: ブロック内に記述された処理でゼロ除算が発生した瞬間、制御が `BEGIN; … END;` の中に飛び、プログラムがアベンディング(Abend)するのを防いでいる。
2. `FIXED` 組み込み関数の使用: 異なる精度やデータ型の演算において、意図しない精度のドロップや予期せぬ例外を防ぐため、ビルトイン関数で明示的にキャストを行っている点に注目してほしい。
3. 二重の防御: 事前に `IF SALES_QTY > 0` で弾くロジックを入れつつ、万が一のデータ化けによるすり抜けに対して `ON ZERODIVIDE` を構える。これがプロの「堅牢なコード」だ。
—
4. システムダンプからゼロ除算の原因を秒速で特定するコツ
もし、上記のようなガードをすり抜けて、あるいはONユニットを仕掛け忘れてプログラムが `IBM0231S` などのメッセージを残してSYSUDUMPを吐いたとき、君はどうやって原因箇所を特定する?
「CEE3201S The condition ZERODIVIDE occurred…」
メインフレームの保守エンジニアなら見慣れたこのエラーメッセージだ。ここから素早く原因に辿り着くための手順を伝授しよう。
1. CEEDUMP / SYSUDUMP のトレースバック(Traceback)を確認する
ダンプリストの最初の方にある「Traceback」セクションを見ろ。異常終了が発生した時点のサブプログラム名、そしてブレークポイント(オフセットアドレス)が記載されている。
2. コンパイラのリストlisting(プロドマップ)と突き合わせる
コンパイル時に出力されたリスト(CBLCARDに `LIST` や `OFFSET` を指定したもの)を開き、トレースバックに記載されているオフセットアドレスと合致する機械語命令(Machine Instruction)を探す。
3. 除算命令(`DR` や `ED` など)の直前のレジスタを確認する
レジスタ・ダンプ(General Purpose Registers)を確認し、除数としてロードされていた汎用レジスタの値がすべて `00000000` になっているのを確認する。「ああ、やっぱりここか」と確信する瞬間だ。
4. VSAMのキーを逆引きする
ダンプ内のワーキングストレージ領域(Working-Storage)に残された直前のレコードバッファを覗き見ろ。どの顧客ID(`CUST_ID`)のレコードを処理している最中にその悲劇が起きたかが一発で分かるはずだ。この瞬間が分かれば、テスト環境で該当データを再現して修正するのは容易い。
—
5. シニアアーキテクトからのメッセージ
PL/Iという言語は、古臭い遺物などではない。メモリー管理の効率、数値演算の正確さ、そしてメインフレームのハードウェア性能を極限まで引き出すために最適化された、極めて洗練された言語だ。
ゼロ除算(ZERODIVIDE)という単純なエラー一つをとっても、言語仕様、コンパイラの挙動、ハードウェアの割込みメカニズム、そしてVSAM等のファイルI/Oとの関係性を正しく理解していれば、恐るるに足りない。
現場で後輩が「先輩、またバッチが落ちました!」と青い顔して駆け込んできたら、慌てず騒がず、この記事で解説した手順を思い出させてやってくれ。
「まずはトレースバックを開け。犯人はデータか、それともコードの怠慢か、ダンプがすべてを語っているはずだ」とね。
それじゃ、今夜のバッチ運用が無事に終わることを祈っている。また現場のどこかで会おう!
