第16章:リトライ戦略(いつ再試行?いつ補償?いつ止める?)⏳🧯
この章のゴール🎯✨
- 「失敗したから、とりあえずリトライ!」を卒業する😇🍼
- 失敗を 分類 して、Retry / Compensate / Halt を決められるようになる🧠📋
- Sagaでありがちな リトライ地獄(無限ループ・二重実行・障害拡大)を避けられる🛑🔥
- C#で「方針どおり」に動くリトライを実装できるようになる🧑💻✨
1) まず結論:リトライは“万能薬”じゃない💊🙅♀️
リトライは便利だけど、使い方を間違えると…👇
- 障害中の相手をさらに殴り続けて 障害を悪化 させる😵💫💥
- すでに成功してたのに、もう一回やって 二重課金 みたいな事故になる😱💳
- Saga全体が前に進めず、永遠にリトライして 誰も助けられない 状態になる🌀🧟♂️
だから、Sagaではこう考えるのが基本だよ👇😊 「リトライして良い失敗」だけリトライする ✅ 「補償すべき失敗」は補償する 🧾🔁 「止めて人間に渡すべき失敗」は止める 🛑👩💼
2) 失敗を“3種類”に分けよう📦🔍
最初にここを分けるだけで、判断がめっちゃラクになるよ😊✨
| 失敗の種類 | 例 | 基本アクション |
|---|---|---|
| ① 一時的なインフラ失敗(Transient)⚡ | ネットワーク瞬断、タイムアウト、相手が一瞬落ちてた、HTTP 503/504 | Retry が効くことが多い🔁 |
| ② 恒久的な失敗(Permanent)🧱 | 入力ミス、権限なし、在庫ゼロ確定、仕様違反 | Retryしても無駄 → 早めに判断🧠 |
| ③ 結果が不明(Unknown outcome)❓ | タイムアウトしたけど相手側で処理されたか不明、レスポンス消えた | “再実行して安全?” を最優先🔑🛡️ |
リトライの前提として「冪等性」や「冪等キー」が超重要だったよね(第9〜10章)🔑✨ これが弱いと、リトライは“事故製造機”になりがち😱
3) 判断フロー(これだけ覚えればOK)🧭✅
リトライ判断の意思決定 🧭✅
-
安全(冪等):同じ要求をもう一回やっても結果が同じになる😊
- 例:
POST /reserve-inventoryでも 冪等キー で「同じ予約」扱いにできるならOK
- 例:
-
危険(非冪等):やるたび結果が増える😱
- 例:冪等キーなしの「課金実行」「ポイント付与」
👉 非冪等なら:まず冪等化(冪等キー / 状態チェック / “すでに処理済み?”照会)を考える🔍✨
ステップB:失敗はTransient?Permanent?Unknown?🧠
- Transientっぽい:タイムアウト、503、接続エラー、429(混雑)など → リトライ候補🔁
- Permanentっぽい:仕様違反、在庫ゼロ確定、支払い拒否確定 → 補償 or 失敗確定🧾
- Unknown:タイムアウト等で「成功したか不明」 → 同じ操作の再実行は慎重に❗
ステップC:リトライ予算(Retry Budget)を超えた?💰⏳
Sagaは“いつか成功するまで”粘ると運用崩壊しがち🫠
- 回数上限(例:最大3〜5回)🔁
- 時間上限(例:合計30秒〜数分)⏱️
- 全体の締め切り(Saga全体のタイムアウト)⛔
超えたら👇
- Compensate(補償) へ🧾🔁
- もしくは Halt(停止して人間へ) 🛑👩💼
4) “いつ補償?いつ止める?”の基準🧾🛑
補償(Compensate)に行く条件🧾🔁
- そのステップが 永久失敗(例:在庫ゼロ確定)だった😇
- 上限までリトライしてもダメ だった😵
- Sagaが途中まで成功していて、これ以上進めない(例:決済成功→在庫確保失敗)💳➡️📦❌
- 相手システムに迷惑をかけそう(混雑・障害中)で、これ以上のRetryが危険⚠️
停止(Halt)に行く条件🛑👩💼
- 結果不明が解消できない(照会もできない、ログも足りない)❓😱
- 補償も失敗し続ける(補償ループ)🌀
- データが壊れてる、想定外の例外、バグ臭い🧨🐛
- “ここから先は人間の判断が必要”な業務(高額取引など)💰🧑⚖️
5) バックオフは「指数 + ゆらぎ」が基本✨🎢
リトライ間隔を一定にすると、みんなが同時に突撃して地獄になる(リトライ嵐)😵💫🌪️ だからよく使うのが👇
- 指数バックオフ:
200ms → 400ms → 800ms → ...⏳ - ジッター(ゆらぎ):ちょっとランダムにずらす🎲
- 429 / 503 のときは相手の指示(Retry-After)を尊重 🙏📩
6) 方針表テンプレ(Retry / Compensate / Halt)📋✨
まずは“表”にすると、設計が一気に安定するよ😊🌸
| ステップ | 失敗例 | Retry? | Compensate? | Halt? |
|---|---|---|---|---|
| 決済(Payment)💳 | タイムアウトで結果不明 | △(照会できるなら) | △(照会で成功なら次へ、失敗なら補償) | ○(照会不能が続くなら) |
| 在庫確保(Inventory)📦 | 503/接続失敗 | ○ | △(上限超えたら補償へ) | △ |
| 配送手配(Shipping)🚚 | 仕様エラー(住所不正) | ✖ | ○(決済済なら返金など) | △(例外的ケース) |
ポイントはこれ👇😊
- “結果不明”は特別扱い(照会して確定させる)🔍❗
- リトライ上限超えたら次の手(補償 or 停止)⛔
7) C#実装①:HTTP呼び出し側に“標準回復”を入れる🧑💻🛡️
HTTPは一時的な失敗が起きやすいので、.NETでは回復性(Resilience)を組み込みやすくなってるよ😊
Microsoft.Extensions.Http.Resilience は Pollyベースで、リトライやタイムアウト等をまとめて扱える(標準ハンドラあり)📦✨ (nuget.org)
ちなみに昔よく見た
Microsoft.Extensions.Http.Pollyは非推奨扱いになってるよ⚠️(新しい方を使おう) (Microsoft Learn)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// 例:Payment API を叩く HttpClient に回復性ハンドラを追加
builder.Services
.AddHttpClient<PaymentClient>(client =>
{
client.BaseAddress = new Uri("https://payment.example/");
client.Timeout = TimeSpan.FromSeconds(10); // これは“最後の砦”として短め推奨⏱️
})
.AddStandardResilienceHandler(); // 標準の回復性(リトライ等)🛡️✨
var app = builder.Build();
await app.RunAsync();
public sealed class PaymentClient
{
private readonly HttpClient _http;
public PaymentClient(HttpClient http) => _http = http;
public Task<HttpResponseMessage> ChargeAsync(HttpRequestMessage req, CancellationToken ct)
=> _http.SendAsync(req, ct);
}
標準ハンドラの中には、全体タイムアウト・リトライ・レート制限・サーキットブレーカー等の“パイプライン”が含まれる設計になってるよ📦🧱 (nuget.org)
(中身をカスタムしたくなったら AddResilienceHandler で自分の戦略を組む感じ😊)
8) C#実装②:Saga側で「Retry予算 → 補償 → 停止」を制御する🎛️🧾🛑
HTTPクライアント側だけだと、Sagaとしての意思決定(補償/停止)ができないよね😇 だからSagaのオーケストレータ側では、こんな責務を持つのがコツ👇
- 何回失敗した?🔁
- どの種類の失敗?⚡🧱❓
- 予算超えた?💰
- 補償に行く?止める?🧾🛑
Polly系の “パイプライン” 構築は AddResiliencePipeline みたいに組めるよ(順序が大事)🧱✨ (Microsoft Learn)
例:Sagaステップ実行ヘルパー(考え方が伝わる最小形)😊
using System.Net;
using Polly;
using Polly.Retry;
public enum StepDecision { Retry, Compensate, Halt }
public static class SagaRetry
{
// ざっくり例:HTTPでよくある「一時的」判定
public static bool IsTransient(HttpStatusCode statusCode)
=> statusCode == HttpStatusCode.TooManyRequests // 429
|| statusCode == HttpStatusCode.RequestTimeout // 408
|| (int)statusCode >= 500; // 5xx
public static StepDecision Decide(
int attempt,
int maxAttempts,
bool idempotent,
bool unknownOutcome,
bool transient)
{
// ① 結果不明 & 非冪等 → まず止める寄り(照会などの別ルート推奨)❗
if (unknownOutcome && !idempotent)
return StepDecision.Halt;
// ② 一時的なら、上限までリトライ🔁
if (transient && attempt < maxAttempts)
return StepDecision.Retry;
// ③ それ以外(永久っぽい / 上限超え)→ 補償へ🧾
return StepDecision.Compensate;
}
public static ResiliencePipeline CreateRetryPipeline(int maxAttempts)
{
// Polly v8系のリトライ戦略(最小例)🔁
return new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = maxAttempts - 1, // “初回 + リトライ回数”の考え方に注意👀
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder()
.Handle<TimeoutException>()
.Handle<HttpRequestException>()
})
.Build();
}
}
ここで大事なのは👇😊
- 「何でも例外ならリトライ」じゃなくて、先に分類してから決める 🧠
- 非冪等 & 結果不明は危険(照会で確定させる/止める)🔑❗
- リトライは上限付き(上限超えたら補償 or 停止)⛔🧾🛑
9) よくある事故パターン集(超重要)😱📚
① “リトライしたら二重課金した”💳💥
- 原因:非冪等操作をそのままリトライ
- 対策:冪等キー、処理済み照会、決済IDの一意制約 など🔑🛡️
② “タイムアウト=失敗”と決めつけて補償したら、実は成功してた😵💫
- 原因:結果不明を失敗扱いにした
- 対策:結果照会API を用意して「成功/失敗」を確定させる🔍✅
③ “障害中に全員がリトライして相手が死んだ”🌪️💀
- 原因:一定間隔リトライ、ジッターなし
- 対策:指数バックオフ + ジッター、必要なら遮断(サーキット)🧯🎲
10) ミニ演習(紙でもOK)📝😊
演習①:失敗を分類しよう📦
次を「Transient / Permanent / Unknown」に分類してみてね👇
- (A)
HttpRequestExceptionが出た - (B) 住所が必須なのに空だった
- (C) 決済APIがタイムアウトした(成功したか不明)
- (D) 在庫0が返ってきた
演習②:方針表を完成させよう📋✨
「注文→決済→在庫→配送」それぞれに対して👇
- リトライ上限は?🔁
- 上限超えたら補償?停止?🧾🛑
- “結果不明”のときはどう確定させる?🔍
11) AI活用(レビューが強い!)🤖✨
コピペで使える感じにしておくね😊💕
- 「このSagaの各ステップについて、Transient/Permanent/Unknown を分類して、Retry/Compensate/Halt の表を作って」📋🤖
- 「非冪等操作が紛れてないかチェックして。二重実行になりそうな箇所を指摘して」🔑👀🤖
- 「“結果不明”が起きた時の照会フロー(成功確定→次へ / 失敗確定→補償 / 確定不能→停止)を書いて」🔍🧾🛑🤖
- 「リトライ嵐になりそうな設定(間隔・回数・同時実行)を指摘して、改善案を出して」🌪️🧯🤖
12) 2026年の“いま”押さえておきたい補足📌✨
- .NETは .NET 10 が最新のLTSで、2026/01/13時点の最新パッチは 10.0.2 だよ📅✨ (Microsoft)
- 回復性(Resilience)は Pollyベースで、
Microsoft.Extensions.Resilience/Microsoft.Extensions.Http.Resilienceが推奨ルートになってるよ🛡️📦 (Microsoft Learn) - Polly自体も継続的に更新されていて、NuGet上では Polly 8.6.5 が確認できるよ🔁📚 (nuget.org)
✅ この章のまとめ(超短く)🌸
- リトライは「一時的な失敗」にだけ効く🔁
- “結果不明”と“非冪等”は特に危険(照会・冪等キー・停止をセットで)❗🔑
- Sagaは Retry予算 → 補償 → 停止 の順で、運用できる形にする🧾🛑✨