おい、若いの。今日も夜間バッチのログに睨めっこか?
……ほう、出たな。「CEE3207S The condition data exception occurred」。
そしてその相棒である、お馴染みのシステム異常終了コード「S0C7」だ。
メインフレームの保守現場にいる我々にとって、このS0C7ほど胃が痛くなるエラーもないだろう。何百万件と流れるVSAMのレコードや順次ファイルの海のどこかに、「人間が想定していない汚いデータ」が紛れ込み、計算の瞬間にコンパイラが「おっと、これは数として読めないぜ!」と音を上げる。それがこのデータ例外の正体だ。
今日は、なぜPL/Iのプログラムがこの罠にハマるのか、そしてあの悪名高いパンチカード時代からの遺産である「パック10進数(COMP-3)」の不都合な真実について、徹底的に叩き込んでやる。耳の穴かっぽじってよく聞きな。
—
1. そもそもなぜS0C7(CEE3207S)が起きるのか?
PL/Iは非常に懐の深い言語だ。CやJavaのように「変数名には予約語を使ってはいけない」なんてケチなことは言わない。PL/Iには明確な予約語が存在しない。
`IF`, `THEN`, `GO TO` などのキーワードですら、上下文(コンテキスト)で判断されるため、変数名として使うことすら理論上はできてしまう(もちろん、そんな悪趣味なコーディングをする奴は現場から叩き出されるがな)。
しかし、その「何でも受け入れる寛容さ」が、データ定義の甘さと結びついたとき、最悪の牙をむく。それがパック10進数(PL/Iでの属性名:`FIXED DECIMAL`)の不正形式だ。
パック10進数は、1バイト(8ビット)に2桁の数字を詰め込み、最後の4ビット(下位ニブル)に符号(`C`や圧密前の正負を表すゾーン、通常は `C`や`F`、負数なら `D` など)が入る構造になっている。
もし、外部ファイル(VSAMやQSAM)から読み込んだデータが、空白(X’40’)だったり、文字データ(X’C1’など、いわゆるアスキーやシフトJISのゴミ、あるいはゾーン10進数の崩れ)のまま `FIXED DECIMAL` の領域に引き渡され、それを算術演算に使った瞬間……ハードウェア(CPU)が「おい、ここにあるのは数字じゃねぇ!」と悲鳴を上げ、S0C7割り込みを発生させるのだ。
—
2. ダンプからの「不正パック10進数」の特定手順
夜間バッチが異常終了し、SYSUDUMPやCEEDUMPが吐き出されたとする。慌ててSDSFのDAやSTを叩く前に、落ち着いて次の手順を踏め。
1. PSW(Program Status Word)の命令アドレスを確認する
ダンプの冒頭にあるPSWから、どの機械語命令(大抵は `PACK`、`UNPK`、`AP`、`SP`、`MP`、`DP` あたりの十進演算命令)で落ちたかを特定する。
2. レジスタ(R0~R15)の指すアドレスを追う
該当命令のオペランド(メモリアドレス)をストレージダンプ(Storage Dump)で表示させる。
3. 下位ニブル(最後の1ニブル)の符号を疑う
表示されたデータの末尾1バイトを見る。本来なら `C`, `D`, `F` などの有効な符号が入るべきところに、文字の `A`(X’C1’)やスペース(X’40’)が入っていたら、それが犯人だ。
「どこかの前工程のバッチが、仕様変更でレイアウトが変わったのに再コンパイルをサボった」とか、「画面からのオンライン入力で数値項目のバリデーションが抜けてブランクがそのままDBやファイルに書き込まれた」……大体原因はこのどちらかだ。
—
3. 実践:ONユニットによる例外トラップと安全なデータ処理
さて、ここからがプロの腕の見せ所だ。
「汚いデータが来たらバッチを異常終了させる」のは、運用としては正しいが、巨大なデータナショナル基幹では、1件のゴミデータのせいで数時間のバッチが巻き戻る(ロールバック)のは避けたいケースもある。あるいは、エラーログに原因データを優しく記録してスキップさせたい場合もあるだろう。
ここで、PL/Iの真骨頂であるONユニット(条件立て割り込み処理)と、組み込み関数(BUILTIN)を活用した実践的なソースコードを示す。大文字で書かれた、現場の標準的なコーディングスタイルをしっかり目に焼き付けておけ。
1
—————————————————————-
- プログラム名: DATCHK01
- 概要: VSAMファイルを読み込み、パック10進数の整合性をチェックしつつ
- データ例外(S0C7)をONユニットで捕捉・回避するサンプル
—————————————————————-
DATCHK01: PROC OPTIONS(MAIN);
— ファイル定義 (VSAM KSDS) —
DCL IN_FILE FILE RECORD INPUT
ENVIRONMENT(BUFSZ(4096));
— レコードレイアウト構造体 —
DCL 1 IN_RECORD,
5 EMP_ID CHAR(5), 従業員ID
5 EMP_NAME CHAR(20), 従業員名
5 RAW_SALARY CHAR(8), 生の入力領域(汚染データ対策)
5 REDEF_SALARY REDEFINES RAW_SALARY,
10 SALARY FIXED DEC(7,2); パック10進数としての給与
— ワーク・フラグ変数 —
DCL EOF_FLAG CHAR(1) INIT(‘0’);
DCL ERR_COUNT FIXED BIN(31) INIT(0);
— ファイル終了(ENDFILE)の条件ハンドラ —
ON ENDFILE(IN_FILE) EOF_FLAG = ‘1’;
— データ例外(CONVERSION / SIZE)の条件ハンドラ —
- S0C7につながる変換エラーやサイズあふれをトラップする
ON CONVERSION BEGIN;
DISPLAY(‘ 警告: 不正な数値データを検知しました ID = ‘ || EMP_ID);
DISPLAY(‘ 問題の生データ(HEX): ‘ || HEX(RAW_SALARY));
- エラー時は安全な初期値(ゼロ)に強制置換して続行する
SALARY = 0;
ERR_COUNT = ERR_COUNT + 1;
END;
— ファイルのオープン —
OPEN FILE(IN_FILE);
— メイン処理ループ —
READ FILE(IN_FILE) INTO(IN_RECORD);
DO WHILE (EOF_FLAG = ‘0’);
- ここで演算を行う。もしRAW_SALARYが汚れていれば、
- 上記のON CONVERSIONユニットが自動的に発動する。
IF SALARY > 1000000 THEN
PUT SKIP EDIT (‘高額所得者:’, EMP_ID, SALARY)
(A, X(2), A, X(2), F(10,2));
END;
READ FILE(IN_FILE) INTO(IN_RECORD);
END;
— 終了処理 —
CLOSE FILE(IN_FILE);
PUT SKIP EDIT (‘処理終了. 変換エラー件数 = ‘, ERR_COUNT)
(A, F(5));
END DATCHK01;
コードの解説とアーキテクトからの助言
1. `REDEFINES` による安全網
ファイルから読み込む時点ではあえて `CHAR(8)` として受け取り、それを `REDEFINES` で `FIXED DEC(7,2)` に重ねている。これにより、いきなりバイナリ演算に巻き込んでハードウェア割り込み(S0C7)を起こす前に、文字としての評価やハンドリングの余地を残しているのがミソだ。
2. `ON CONVERSION` ユニットの活用
PL/Iでは、データ型の不一致や不正文字の数値変換時に `CONVERSION` 条件が発生する。これを利用して、システム全体がクラッシュするのを防ぎ、ログに `HEX` 組み込み関数でダンプを吐かせることで、後続の調査を劇的に楽にできる。
3. 組み込み関数(BUILTIN)の積極利用
`HEX()` や `LENGTH()` などの組み込み関数は、コンパイラが最適化された機械語コードを吐いてくれるため、自前で変換ロジックを書くよりもはるかに高速かつ安全だ。
—
4. 先輩からの教訓:根本治療を忘れるな
ONユニットでトラップして処理を継続させるコードを書いたからといって、「これで安心だね」などと油断するなよ?
それはあくまで「ダムが決壊したときに、とりあえず土嚢を積んで家を守る」対症療法に過ぎない。
真に解決すべきは、なぜその「汚いデータ」が上流から流れてきたのかという根本原因(Root Cause)の究明だ。
上流システムのインターフェース仕様書を確認し、データバリデーション(妥当性チェック)の抜け穴をふさぐこと。それができて初めて、一人前のメインフレーム・エンジニアと言える。
レガシーシステムの寿命は長い。だからこそ、こうした泥臭いデータの機微を理解している我々のようなエンジニアの価値は、これからも決して色あせることはない。
さあ、コーヒーでも飲んで、さっきのダンプの続きを解析するとしようか。質問があればいつでも来い。
