よくあるアンチパターン
よくあるアンチパターン
Section titled “よくあるアンチパターン”FastAPIでよくあるアンチパターンと、実際に事故った構造を詳しく解説します。
A. リソースの「垂れ流し」
Section titled “A. リソースの「垂れ流し」”実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: 例外を握りつぶしてファイルやDB接続を閉じないdef process_file(file_path: str): file = open(file_path, 'r') try: # ファイル処理... pass except Exception as e: # 問題: 例外を握りつぶしてファイルを閉じない logger.error(f"File processing failed: {e}") # ファイルが閉じられず、ファイル記述子がリーク # 問題: finallyブロックがないため、正常終了時もファイルが閉じられないなぜ事故るか:
- ファイル記述子の枯渇: ファイルが閉じられず、OSのファイル記述子が枯渇する
- 接続リーク: DB接続が閉じられず、接続プールが枯渇する
- 数時間後の停止: リソースリークは数時間後にシステム全体を停止させる
設計レビューでの指摘文例:
【指摘】リソースが適切に解放されていません。【問題】例外時にファイルやDB接続が閉じられず、リソースリークが発生します。【影響】ファイル記述子・接続プールの枯渇、数時間後のシステム停止【推奨】try-finallyブロックまたはwith文で確実にリソースを解放するB. 無防備な待機
Section titled “B. 無防備な待機”実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: 外部API呼び出しにタイムアウトを設定しないasync def create_order(order_data: OrderData) -> Order: async with db.begin() as tx: # 1. 注文を作成(データベースに保存) order = Order(**order_data.dict()) tx.add(order) await tx.commit()
# 2. トランザクション内で外部APIを呼ぶ(問題) # 問題: タイムアウトが設定されていない # 問題: サーキットブレーカーがない async with httpx.AsyncClient() as client: response = await client.post( 'https://payment-api.example.com/charge', json={'order_id': order.id, 'amount': order_data.amount}, )
if response.status_code != 200: raise PaymentError("Payment failed")
# 3. 決済結果を保存 order.payment_status = "COMPLETED" await tx.commit()
return orderなぜ事故るか:
- トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
- 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
- ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
- タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする
- イベントループのブロック: 遅延が連鎖してイベントループがブロックされ、全エンドポイントが応答不能に
実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: トランザクション内で外部APIを呼ぶfrom sqlalchemy.orm import Sessionimport httpx
async def create_order(db: Session, order_data: OrderData) -> Order: # 1. 注文を作成(データベースに保存) order = Order(**order_data.dict()) db.add(order) db.commit()
# 2. トランザクション内で外部APIを呼ぶ(問題) async with httpx.AsyncClient() as client: response = await client.post("https://payment-api.example.com/charge", json={ "orderId": order.id, "amount": order_data.amount, })
if response.status_code != 200: raise PaymentError("Payment failed")
# 3. 決済結果を保存 order.payment_status = "COMPLETED" db.commit()
return orderなぜ事故るか:
- トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
- 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
- ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
設計レビューでの指摘文例:
【指摘】トランザクション内で外部APIを呼んでいます。【問題】外部APIの応答を待つ間、データベースのロックが保持されます。【影響】パフォーマンスの低下、デッドロックの発生、タイムアウトのリスク【推奨】Outboxパターンを使用し、トランザクション外で外部APIを呼ぶ2. 同期処理と非同期処理の混在
Section titled “2. 同期処理と非同期処理の混在”実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: 同期処理と非同期処理の混在from fastapi import FastAPIimport requests
app = FastAPI()
@app.post("/orders")async def create_order(order_data: OrderData): # 問題: 非同期関数内で同期I/O操作 response = requests.post("https://api.example.com/charge", json=order_data.dict()) # イベントループがブロックされる return response.json()なぜ事故るか:
- イベントループのブロック: 同期I/O操作により、イベントループがブロックされる
- 他のリクエストの処理不可: イベントループがブロックされている間、他のリクエストが処理できない
- パフォーマンスの低下: アプリケーション全体のパフォーマンスが低下する
設計レビューでの指摘文例:
【指摘】非同期関数内で同期I/O操作を使用しています。【問題】同期I/O操作により、イベントループがブロックされます。【影響】イベントループのブロック、他のリクエストの処理不可、パフォーマンスの低下【推奨】httpx.AsyncClientなどの非同期HTTPクライアントを使用するよくあるアンチパターンのポイント:
- A. リソースの「垂れ流し」: 例外を握りつぶしてファイルやDB接続を閉じない → 数時間後にシステム停止
- B. 無防備な待機: 外部API呼び出しにタイムアウトを設定しない → イベントループのブロック、全エンドポイントの応答不能
- C. 非冪等な再試行: 再送時にデータが二重登録される → データの不整合、ビジネスロジックの破綻
- D. 同期I/O操作: イベントループのブロック、パフォーマンスの低下
これらのアンチパターンを避けることで、安全で信頼性の高いFastAPIアプリケーションを構築できます。
重要な原則: 「正常に動く」よりも「異常時に安全に壊れる」ことを優先する。