在庫ゼロと取得失敗は同じ?|例外処理・ログ・監視
在庫の取得が失敗したとき、「在庫0」と返してもよいのでしょうか。利用者には売切れに見え、運用担当は障害に気付きにくくなります。失敗をどこで判断し、何を返し、何を記録するかを分けて考えます。
事例:在庫APIの先で、タイムアウトが起きる
画面が在庫APIへ問い合わせ、APIは保存先から個数を読みます。個数0は正常な売切れです。一方、保存先が時間内に返さない場合は個数が分からず、一時的に利用できない状態とします。
正常な0と、取得できなかった失敗を区別します。この事例では取得失敗に503と「在庫を確認できません」を返し、運用向けには原因と問い合わせを結び付けて記録します。
個数が不明な失敗を、正常な売切れへ変換しません。
例外を捕まえる場所は、どこがよい?
取得処理は、保存先のタイムアウトを「在庫を取得できない」という業務側の失敗へ変えます。APIの境界でその失敗を捕捉し、応答とログを決めます。各層が同じ例外を何度も記録すると、件数や原因を読み違えやすくなります。
- 取得層
保存先固有の失敗を、呼出し側が理解できる例外へ変換します。変換前の原因も保持します。
- API境界
利用者向けの応答を選び、調査できる情報を記録します。秘密の値や詳細な内部構成を応答へ出しません。
- 最上位の境界
想定外の失敗を処理し、内部エラーとして記録・応答します。正常値へ置き換えて隠さないことが重要です。
結果 | この事例の扱い | 理由 |
|---|---|---|
個数0 | 200・在庫0 | 正常な値 |
保存先のタイムアウト | 503・確認できない | 一時的な利用不能 |
個数が負数など | 不正なデータとして別の境界へ伝播 | タイムアウトではない障害 |
プログラム上の想定外の例外 | 最上位で内部エラーとして扱う | 正常な個数を作ってはいけない |
ここでのHTTP応答は、このAPIが保存先の一時的な利用不能として扱う設計です。APIがゲートウェイとして振る舞う場合などは別のステータスが適切なこともあり、責務と契約に合わせて決めます。
短いコードで、失敗と正常値を分ける
class StorageUnavailable(RuntimeError):
pass
def read_stock(load):
try:
quantity = load()
except TimeoutError as exc:
raise StorageUnavailable("inventory backend timeout") from exc
if type(quantity) is not int or quantity < 0:
raise ValueError("invalid stock")
return quantity
def stock_response(load, logger, request_id):
try:
quantity = read_stock(load)
except StorageUnavailable:
logger.exception("stock_read_timeout",
extra={"request_id": request_id})
return {"status": 503, "message": "在庫を確認できません"}
return {"status": 200, "stock": quantity}loadは取得処理を渡す引数です。logger.exceptionは例外を捕捉した場所で呼び、トレースバックを記録します。request_idは同じ問い合わせを追うための値で、データベースの内部主キーとは役割が異なります。
raise ... from excで、元の原因を残します。このコードはTimeoutErrorだけを変換します。不正な個数のValueErrorや別の障害は上位へ伝わり、アプリ全体の境界で記録・応答する設計です。
実装に組み込むときは、使うライブラリがどの型の例外を返すかを確認します。HTTPクライアント固有のタイムアウトなどを、この例のTimeoutErrorと無条件に同一視しないことが必要です。
後片付けで、元の失敗を消していない?
ファイルや接続を開いたままにしないため、withなどの資源管理やfinallyを使います。ただし、finallyでreturnすると、先に起きた例外や戻り値が隠れる場合があります。後片付けと結果の決定を混ぜないようにします。
例外を捕まえた後に何もせず続けると、半分だけ更新された状態で後続処理が動くことがあります。トランザクションの取消し、再開できる地点、既に完了した外部処理を確認します。
再試行すれば、失敗は解決する?
読み取りを1回200ms、最大3回行うなら、各回の最大待ちは合計600msです。待ち間隔や追加処理は別に足します。呼出し元の期限が500msなら、その条件のまま最大3回待つ計画は合いません。
更新処理のタイムアウトは、未実行を意味しません。保存先では確定し、応答だけ届かなかった可能性があります。注文の重複を防ぐキーや結果の照会など、同じ依頼を重ねても二重に更新しない条件が必要です。
大量の再試行は障害中の負荷を増やします。回数、全体の期限、待ち間隔、再試行する失敗の種類を決め、必要なら一時的に呼出しを抑制します。
ログ・メトリクス・トレースは、どう使い分ける?
ログは個々の出来事と原因、メトリクスは失敗率や遅延の傾向、トレースは一つの処理がどのサービスを通りどこで待ったかを調べる手掛かりです。目的が異なるため、必要な情報をつなげます。
- 1. 集計
- 2. 出来事と原因
- 3. 処理の区間
- 4. 異常を検知
- 5. 詳細を確認
- 6. 待つ区間を確認
時刻・問い合わせ・版を結び付けます。相関があるだけで原因を断定はしません。
ログにはUTCなど基準をそろえた時刻、イベント名、処理の結果、所要時間、問い合わせの識別子、プログラムの版を必要に応じて含めます。パスワード、認証トークン、不要な個人情報や本文全体は記録しません。
外部から来る識別子を使うなら長さや文字を検証し、ログの改行混入も防ぎます。記録に失敗した場合の扱い、閲覧権限、保存期間も決め、必要な情報を必要な範囲で残します。
演習1:正常な売切れ
条件:保存先が正常に0を返します。
問い:掲載コードの応答は何ですか。
解答例:statusが200、stockが0です。
根拠:0は非負の整数なので正常な取得結果です。
誤答の理由:0を取得失敗として503へ変えると、正常な売切れを障害と扱ってしまいます。
演習2:タイムアウトの伝播
条件:loadがTimeoutErrorを発生させます。
問い:取得層とAPI境界でどう扱いますか。
解答例:原因を残したStorageUnavailableへ変え、境界で一度記録して503を返します。
根拠:限定した例外型を、それぞれの責務で扱っています。
誤答の理由:例外を捕まえて0を返すと、個数が不明な状態が隠れます。
演習3:想定外のデータ
条件:loadが-1を返します。
問い:503を返す経路へ入りますか。
解答例:入りません。ValueErrorが発生し、上位の境界へ伝播します。
根拠:掲載コードはStorageUnavailableだけを捕捉します。
誤答の理由:すべての例外を同じ取得失敗と扱うと、不正なデータと一時的な通信障害を区別できません。
演習4:更新の再試行
条件:注文登録後の応答だけがタイムアウトした可能性があります。
問い:同じ注文をそのまま再登録してよいですか。
解答例:重複を防ぐ条件や結果照会を設けてから再試行します。
根拠:相手側では既に登録済みかもしれません。
誤答の理由:応答がないから未実行と判断すると、二重の注文を作る可能性があります。
演習5:記録する項目
条件:問い合わせの特定が必要で、認証トークンも受け取ります。
問い:何をログへ残し、何を除外しますか。
解答例:検証した問い合わせ識別子や結果・時刻・版を残し、認証トークンなどの秘密は除外します。
根拠:調査に必要な情報と秘密を分けます。
誤答の理由:全リクエストを無条件に保存すると、調査用ログへ認証情報が流出します。
演習6:遅い区間の調査
条件:失敗率が上がり、複数サービスを経由します。
問い:観測情報をどう使いますか。
解答例:メトリクスで異常を検知し、同じ問い合わせのログとトレースで原因や待ち区間を調べます。
根拠:集計と個別の出来事、経路を結び付けます。
誤答の理由:失敗率だけから原因を断定すると、通信、保存先、アプリのどの区間かを取り違えます。
出典と仕様を確認する
関連テーマを続けて学ぶ
次におすすめの学習
編集・検証について
編集・検証:IT資格ラボ編集部
IPAが公開する試験要綱・シラバス・過去問題と、各技術の公式資料を優先して内容を確認しています。制度変更や誤りを確認した場合は、記事を見直して更新します。
編集方針・情報源・訂正方針を見る