【実務・中級編】CEE3207S(データ例外)の発生原因とパック10進数の不正形式 – PL/Iの基本構文とデータ制御実践ガイド

おい、若いの。今日も夜間バッチのログに睨めっこか?
……ほう、出たな。「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)の究明だ。
上流システムのインターフェース仕様書を確認し、データバリデーション(妥当性チェック)の抜け穴をふさぐこと。それができて初めて、一人前のメインフレーム・エンジニアと言える。

レガシーシステムの寿命は長い。だからこそ、こうした泥臭いデータの機微を理解している我々のようなエンジニアの価値は、これからも決して色あせることはない。
さあ、コーヒーでも飲んで、さっきのダンプの続きを解析するとしようか。質問があればいつでも来い。

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