コンテンツにスキップ
検索語を入力してください

    インフラの耐障害性

    リトライ、タイムアウト、サーキットブレーカーはインフラアダプターの関心事である。ドメイン遷移やユースケースに置くと、ビジネス上リトライすべきでない失敗(認可拒否、バリデーション)まで隠し、冪等性のないコマンドが二重適用される。

    エラーの層分けは エラーハンドリング、冪等キーとアウトボックスは 永続化、集約、イベント、ポートの形は アプリケーション配線 と整合させる。

    関心事レイヤー表現
    「リクエストが見つからない」Application / domainErr(RequestNotFound(...))
    パートナー API からの一時的 HTTP 503Infrastructureバックオフ付きリトライ、その後 raise またはマップ
    DB 接続タイムアウトInfrastructureraise。フレームワークまたはジョブランナーがリトライしうる
    リトライ時の重複コマンドApplication + persistence冪等性キー(永続化、集約、イベント を参照)

    ドメインコードは tenacity、サーキットブレーカーライブラリ、HTTPクライアントリトライミドルウェアをインポートしてはならない。

    アダプター境界で tenacity またはHTTPクライアントの組み込みリトライ方針を使う。

    Terminal window
    uv add tenacity

    一時的で冪等な操作だけをリトライする:

    • 副作用のない安全なGET/読み取り呼び出し。
    • 冪等性キーとデータベース重複排除を含む書き込み。
    • コミット後のアウトボックスリレー公開。

    冪等性保護なしに二重課金、二重割当、重複通知を起こしうるユースケースを盲目的にリトライしない。

    from tenacity import retry, stop_after_attempt, wait_exponential
    @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5, max=8))
    async def fetch_driver_profile(client: httpx.AsyncClient, driver_id: UUID) -> DriverProfileDto:
    response = await client.get(f"/drivers/{driver_id}", timeout=5.0)
    response.raise_for_status()
    return DriverProfileDtoAdapter.validate_python(response.json())

    枯渇したリトライをアダプターエッジで安定したインフラ例外またはユースケースエラーにマップする。生の httpx またはドライバー例外型をポートプロトコル経由で漏らさない。

    インフラ失敗が例外のままか Err になるかは エラーハンドリング を読む。

    シナリオstopwaitretry 述語備考
    冪等 GET / 読み取りstop_after_attempt(3–5)wait_exponential(multiplier=0.5, max=8)HTTP 502/503/504、タイムアウトパートナー読み取りの安全なデフォルト
    アウトボックス公開stop_after_attempt(10)exponential + jitterブローカーエラー、タイムアウトコンシューマー側 event_id 重複排除と組み合わせる
    起動時 DB 接続stop_after_delay(60)fixed 1sOperationalErrorコンポジションルートのみ
    決済 / charge POST盲目的リトライなし冪等性キー + 409/既知安全レスポンス後の単一リトライのみ
    楽観的ロック競合アダプターでリトライしないユースケースが再読み込みして判断
    レート制限 429stop_after_attempt(5)wait_exponential + Retry-After 尊重429 のみ合計待機を SLA 以下に上限
    from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
    @retry(
    retry=retry_if_exception_type(httpx.TimeoutException),
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=0.5, max=8),
    reraise=True,
    )
    async def fetch_with_timeout(...) -> DriverProfileDto:
    ...

    before_sleep ロギングに相関IDを入れる。レスポンスボディは入れない。wait_random_exponential のジッターは、共有依存関係へのサンダリング・ヘアドを抑える。

    発信呼び出しに上限がないと、1つの遅い依存がワーカーやイベントループ全体を占有し、ユーザーには「全体が遅い」ように見える。HTTPクライアント、DBステートメント、キューポール、SDK操作には、それぞれ明示的なタイムアウトを設定する。

    • クライアントでリクエストごとのタイムアウトを優先(httpx/aiohttpの timeout=...)。
    • タイムアウトがビジネスルールの一部である場合を除き(これは稀である)、ドメイン層とユースケース関数から asyncio.wait_for を除く。
    • 呼び出し側が最悪レイテンシを知る必要があるとき、ポートプロトコルにSLA期待を文書化する。
    async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=2.0)) as client:
    ...

    下流の依存関係が継続的に失敗するときは、高速失敗でサービスを保護し、リトライ嵐を避けるためにサーキットブレーカーを使う。

    よくあるライブラリ:

    • pybreaker
    • サービスメッシュまたはAPIゲートウェイの耐障害機能(すでに標準化されているなら優先)

    アダプター実装を包む。ユースケースではない:

    breaker = CircuitBreaker(fail_max=5, reset_timeout=30)
    async def call_partner_api(...) -> PartnerResponseDto:
    return await breaker.call_async(_do_call, ...)

    ブレーカーがopenのときは、安定した劣化モードエラーを返すか、後で処理するようキューに入れる。ブレーカー状態をドメインライフサイクル状態として表現しない。

    サーキットブレーカー状態機械

    Section titled “サーキットブレーカー状態機械”
    failures >= fail_max reset_timeout elapsed trial call succeeds trial call fails success / failures below threshold Closed Open HalfOpen
    状態振る舞い呼び出し側の体験
    Closedすべての呼び出しが通過。失敗をカウント通常レイテンシまたはマップ済みエラー
    Open依存に当たらず高速失敗安定した ServiceUnavailable / キュー投入
    Half-open1 回の試行呼び出しのみ許可成功で close、失敗で再 open

    設定指針:

    • fail_max: HTTPパートナーでは連続失敗5–10。エラーバジェットから調整。
    • reset_timeout: 30–120s。非クリティカル読み取りは短く、過負荷コアは長く。
    • すでにデプロイ済みならmesh/ゲートウェイブレーカーを優先。すべての呼び出し側を保護する。
    • メトリクスを発行: breaker_statebreaker_trips_totalbreaker_rejected_calls_total
    • closed 状態の内側でのみtenacityと組み合わせる。open状態と戦うネストリトライループは作らない。

    冪等性とアウトボックスとの相互作用

    Section titled “冪等性とアウトボックスとの相互作用”

    インフラレイヤーのリトライは、アプリケーション冪等性を補完するが置き換えない:

    1. ユースケースがビジネス前提条件を確認し、状態 + イベントを構築する。
    2. リポジトリが idempotency_key とバージョンチェック付きで原子性保存する。
    3. アウトボックスワーカーが独自のバックオフで公開をリトライする。
    4. 外部APIアダプターは操作が安全またはキー付きのときだけリトライする。

    N回失敗してから成功するフェイクでリトライパスをテストし、重複配信下での一意制約と冪等性キーを統合テストで検証する。

    リトライは冪等性と組み合わされているか — High

    Section titled “リトライは冪等性と組み合わされているか — High”

    冪等キーや重複排除レコードなしに、副作用を二重適用しうるリトライコマンド、アウトボックスプロセッサ、外部API呼び出しを指摘する。

    永続化、集約、イベント と照合する。

    タイムアウトとサーキットブレーカーは明示的か — Medium

    Section titled “タイムアウトとサーキットブレーカーは明示的か — Medium”

    プロジェクトがタイムアウトとブレーカー期待を文書化しているのに、アダプターからの無制限HTTP/DB/キュー呼び出しを指摘する。

    リトライはインフラアダプターに留まっているか — Medium

    Section titled “リトライはインフラアダプターに留まっているか — Medium”

    ドメインモジュールや遷移関数内のリトライデコレータ、スリープループ、サーキットブレーカーを指摘する。

    耐障害ポリシーがドメイン失敗を隠していないか — Medium

    Section titled “耐障害ポリシーがドメイン失敗を隠していないか — Medium”

    リトライすべきでないバリデーション失敗、認可拒否、ビジネスルール拒否を隠しうる、あらゆる例外への広いリトライを指摘する。