インフラの耐障害性
リトライ、タイムアウト、サーキットブレーカーはインフラアダプターの関心事である。ドメイン遷移やユースケースに置くと、ビジネス上リトライすべきでない失敗(認可拒否、バリデーション)まで隠し、冪等性のないコマンドが二重適用される。
エラーの層分けは エラーハンドリング、冪等キーとアウトボックスは 永続化、集約、イベント、ポートの形は アプリケーション配線 と整合させる。
レイヤーごとの責務
Section titled “レイヤーごとの責務”| 関心事 | レイヤー | 表現 |
|---|---|---|
| 「リクエストが見つからない」 | Application / domain | Err(RequestNotFound(...)) |
| パートナー API からの一時的 HTTP 503 | Infrastructure | バックオフ付きリトライ、その後 raise またはマップ |
| DB 接続タイムアウト | Infrastructure | raise。フレームワークまたはジョブランナーがリトライしうる |
| リトライ時の重複コマンド | Application + persistence | 冪等性キー(永続化、集約、イベント を参照) |
ドメインコードは tenacity、サーキットブレーカーライブラリ、HTTPクライアントリトライミドルウェアをインポートしてはならない。
アダプター境界で tenacity またはHTTPクライアントの組み込みリトライ方針を使う。
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 になるかは エラーハンドリング を読む。
Tenacity 戦略決定表
Section titled “Tenacity 戦略決定表”| シナリオ | stop | wait | retry 述語 | 備考 |
|---|---|---|---|---|
| 冪等 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 1s | OperationalError | コンポジションルートのみ |
| 決済 / charge POST | 盲目的リトライなし | — | — | 冪等性キー + 409/既知安全レスポンス後の単一リトライのみ |
| 楽観的ロック競合 | アダプターでリトライしない | — | — | ユースケースが再読み込みして判断 |
| レート制限 429 | stop_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 のジッターは、共有依存関係へのサンダリング・ヘアドを抑える。
タイムアウト
Section titled “タイムアウト”発信呼び出しに上限がないと、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: ...サーキットブレーカー
Section titled “サーキットブレーカー”下流の依存関係が継続的に失敗するときは、高速失敗でサービスを保護し、リトライ嵐を避けるためにサーキットブレーカーを使う。
よくあるライブラリ:
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 “サーキットブレーカー状態機械”| 状態 | 振る舞い | 呼び出し側の体験 |
|---|---|---|
| Closed | すべての呼び出しが通過。失敗をカウント | 通常レイテンシまたはマップ済みエラー |
| Open | 依存に当たらず高速失敗 | 安定した ServiceUnavailable / キュー投入 |
| Half-open | 1 回の試行呼び出しのみ許可 | 成功で close、失敗で再 open |
設定指針:
fail_max: HTTPパートナーでは連続失敗5–10。エラーバジェットから調整。reset_timeout: 30–120s。非クリティカル読み取りは短く、過負荷コアは長く。- すでにデプロイ済みならmesh/ゲートウェイブレーカーを優先。すべての呼び出し側を保護する。
- メトリクスを発行:
breaker_state、breaker_trips_total、breaker_rejected_calls_total。 - closed 状態の内側でのみtenacityと組み合わせる。open状態と戦うネストリトライループは作らない。
冪等性とアウトボックスとの相互作用
Section titled “冪等性とアウトボックスとの相互作用”インフラレイヤーのリトライは、アプリケーション冪等性を補完するが置き換えない:
- ユースケースがビジネス前提条件を確認し、状態 + イベントを構築する。
- リポジトリが
idempotency_keyとバージョンチェック付きで原子性保存する。 - アウトボックスワーカーが独自のバックオフで公開をリトライする。
- 外部APIアダプターは操作が安全またはキー付きのときだけリトライする。
N回失敗してから成功するフェイクでリトライパスをテストし、重複配信下での一意制約と冪等性キーを統合テストで検証する。
レビュー観点
Section titled “レビュー観点”リトライは冪等性と組み合わされているか — High
Section titled “リトライは冪等性と組み合わされているか — High”冪等キーや重複排除レコードなしに、副作用を二重適用しうるリトライコマンド、アウトボックスプロセッサ、外部API呼び出しを指摘する。
永続化、集約、イベント と照合する。
タイムアウトとサーキットブレーカーは明示的か — Medium
Section titled “タイムアウトとサーキットブレーカーは明示的か — Medium”プロジェクトがタイムアウトとブレーカー期待を文書化しているのに、アダプターからの無制限HTTP/DB/キュー呼び出しを指摘する。
リトライはインフラアダプターに留まっているか — Medium
Section titled “リトライはインフラアダプターに留まっているか — Medium”ドメインモジュールや遷移関数内のリトライデコレータ、スリープループ、サーキットブレーカーを指摘する。
耐障害ポリシーがドメイン失敗を隠していないか — Medium
Section titled “耐障害ポリシーがドメイン失敗を隠していないか — Medium”リトライすべきでないバリデーション失敗、認可拒否、ビジネスルール拒否を隠しうる、あらゆる例外への広いリトライを指摘する。