dotnet-resilience について
dotnet-resilienceは、Polly v8を使ったモダンなレジリエンスパターンを扱う.NET開発者向けのスキルです。HTTPと非HTTPのパイプライン、依存性注入、テレメトリー、旧HTTP Pollyパッケージからの移行を説明します。
主な機能
- 01HTTP標準レジリエンスパイプライン
- 02リトライ、タイムアウト、サーキットブレーカー
- 03Polly v8のカスタムパイプライン
- 04依存性注入によるパイプライン登録
- 05レジリエンスのメトリクスとログ
- 06旧HTTP Pollyからの移行
活用シーン
- 01HTTPクライアントにリトライとタイムアウトを設定
- 02データベース呼び出し向けのパイプライン構築
- 03サーキットブレーカーとリトライ設定の調整
- 04プロジェクトをモダンなレジリエンスパッケージへ移行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-resilienceインストール後、Claude Code で/dotnet-resilienceを入力して読み込まれたか確認します。
詳細
dotnet-resilience
Polly v8 と Microsoft.Extensions.Http.Resilience を使用した、.NET アプリケーション向けの最新のレジリエンスパターンです。標準のレジリエンスパイプライン(レートリミッター、全体タイムアウト、再試行、サーキットブレーカー、試行タイムアウト)、カスタムパイプラインの設定、.NET の依存性注入システムとの統合について説明します。
置き換えられたパッケージ: Microsoft.Extensions.Http.Polly は Microsoft.Extensions.Http.Resilience に置き換えられています。新しいプロジェクトでは Microsoft.Extensions.Http.Polly を使用しないでください。既存のコードをアップグレードする方法については、移行ガイドを参照してください。
対象外: DI コンテナーの仕組みとサービスの有効期間については、[skill:dotnet-csharp-dependency-injection]を参照してください。非同期/await パターンとキャンセルトークンの伝播については、[skill:dotnet-csharp-async-patterns]を参照してください。HTTP クライアントファクトリーパターン(型指定クライアント、名前付きクライアント、DelegatingHandlers)については、[skill:dotnet-http-client]で説明しています。レジリエンスポリシーのテストについては、WebApplicationFactory を使ったテストは[skill:dotnet-integration-testing]を、レジリエンスハンドラーの単体テストは[skill:dotnet-xunit]を参照してください。
相互参照:サービスの登録については[skill:dotnet-csharp-dependency-injection]、キャンセルトークンの伝播については[skill:dotnet-csharp-async-patterns]、HTTP クライアントにレジリエンスを適用する方法については[skill:dotnet-http-client]を参照してください。
パッケージ一覧
| パッケージ | ステータス | 用途 |
|---|---|---|
Polly (v8+) | 現行 | 中核となるレジリエンスライブラリ -- 戦略、パイプライン、テレメトリ |
Microsoft.Extensions.Resilience | 現行 | HTTP 以外のレジリエンスパイプライン向け DI 統合 |
Microsoft.Extensions.Http.Resilience | 現行 | IHttpClientFactory のレジリエンスパイプライン向け DI 統合 |
Microsoft.Extensions.Http.Polly | 置き換え済み | 従来の HTTP レジリエンス -- Microsoft.Extensions.Http.Resilience に移行してください |
Polly (v7 and earlier) | レガシー | 旧 API -- v8 に移行してください |
最新のスタックをインストールします。
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.*" />
HTTP 以外のシナリオのみの場合:
<PackageReference Include="Microsoft.Extensions.Resilience" Version="9.*" />
標準のレジリエンスパイプライン
Microsoft.Extensions.Http.Resilience は、推奨される順序に従った標準のレジリエンスパイプラインを提供します。パイプラインの各レイヤーは、最も外側から最も内側へと実行されます。
Request
--> Rate Limiter (1. shed excess load)
--> Total Timeout (2. cap total wall-clock time)
--> Retry (3. retry transient failures)
--> Circuit Breaker (4. stop calling failing services)
--> Attempt Timeout (5. cap individual attempt time)
--> HTTP call
この順序が重要な理由
- 最初にレートリミッターを配置:再試行の集中によって下流サービスに過大な負荷がかかるのを防ぎます
- 全体タイムアウトが再試行を包む:すべての再試行を含む操作全体に期限を設定します
- 再試行がサーキットブレーカーを包む:ブレーカーがリセットされた後に再試行できます。回路が遮断された状態は、再試行可能な失敗として扱われます
- サーキットブレーカーが試行タイムアウトを包む:タイムアウトした試行は、ブレーカーの失敗しきい値に加算されます
- 最も内側に試行タイムアウトを配置:個々の HTTP 呼び出しにそれぞれ期限を設定します
既定値を使用する標準パイプライン
builder.Services
.AddHttpClient("catalog-api", client =>
{
client.BaseAddress = new Uri("https://catalog.internal");
})
.AddStandardResilienceHandler();
妥当な既定値で標準パイプラインを適用します。
- レートリミッター:同時リクエスト数 1000
- 全体タイムアウト:30 秒
- 再試行:3 回、指数バックオフ(基準 2 秒)、ジッターあり
- サーキットブレーカー:失敗率 10%、サンプルサイズ 100、遮断時間 5 秒
- 試行タイムアウト:10 秒
カスタムオプションを使用する標準パイプライン
builder.Services
.AddHttpClient("catalog-api", client =>
{
client.BaseAddress = new Uri("https://catalog.internal");
})
.AddStandardResilienceHandler(options =>
{
// Total timeout for the entire operation including retries
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);
// Retry strategy
options.Retry.MaxRetryAttempts = 5;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.Retry.UseJitter = true;
options.Retry.ShouldHandle = args => ValueTask.FromResult(
args.Outcome.Result?.StatusCode is
HttpStatusCode.RequestTimeout or
HttpStatusCode.TooManyRequests or
>= HttpStatusCode.InternalServerError);
// Circuit breaker
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
options.CircuitBreaker.FailureRatio = 0.1;
options.CircuitBreaker.MinimumThroughput = 20;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(10);
// Per-attempt timeout
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
});
appsettings.json による設定
環境ごとに調整できるよう、設定からレジリエンスオプションをバインドします。
builder.Services
.AddHttpClient("catalog-api", client =>
{
client.BaseAddress = new Uri("https://catalog.internal");
})
.AddStandardResilienceHandler(options =>
{
builder.Configuration
.GetSection("Resilience:CatalogApi")
.Bind(options);
});
{
"Resilience": {
"CatalogApi": {
"Retry": {
"MaxRetryAttempts": 5,
"Delay": "00:00:02",
"BackoffType": "Exponential"
},
"CircuitBreaker": {
"BreakDuration": "00:00:15"
},
"TotalRequestTimeout": {
"Timeout": "00:01:00"
}
}
}
}
カスタムレジリエンスパイプライン
標準パイプラインが適さない場合は、Polly v8 を直接使用してカスタムパイプラインを構築します。
再試行戦略
builder.Services.AddResiliencePipeline("db-retry", pipelineBuilder =>
{
pipelineBuilder.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder()
.Handle<DbUpdateConcurrencyException>()
.Handle<TimeoutException>(),
OnRetry = args =>
{
// Structured logging of retry attempts
var logger = args.Context.Properties
.GetValue(new ResiliencePropertyKey<ILogger>("logger"), null!);
logger?.LogWarning(
args.Outcome.Exception,
"Retry attempt {AttemptNumber} after {Delay}ms",
args.AttemptNumber,
args.RetryDelay.TotalMilliseconds);
return ValueTask.CompletedTask;
}
});
});
// Inject and use
public sealed class OrderRepository(
[FromKeyedServices("db-retry")] ResiliencePipeline pipeline,
AppDbContext db)
{
public async Task<Order> UpdateAsync(Order order, CancellationToken ct)
{
return await pipeline.ExecuteAsync(async token =>
{
db.Orders.Update(order);
await db.SaveChangesAsync(token);
return order;
}, ct);
}
}
サーキットブレーカー戦略
builder.Services.AddResiliencePipeline("payment-gateway", pipelineBuilder =>
{
pipelineBuilder.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
SamplingDuration = TimeSpan.FromSeconds(30),
FailureRatio = 0.25, // Open after 25% failure rate
MinimumThroughput = 10, // Need at least 10 calls to evaluate
BreakDuration = TimeSpan.FromSeconds(15),
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.Handle<TimeoutException>()
});
});
タイムアウト戦略
builder.Services.AddResiliencePipeline("external-api", pipelineBuilder =>
{
// Total timeout for the entire pipeline execution
pipelineBuilder.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(30),
OnTimeout = args =>
{
// Log timeout details for diagnostics
return ValueTask.CompletedTask;
}
});
});
複数の戦略の組み合わせ
戦略を連結して複合パイプラインを構築します。順序が重要です。最も外側の戦略を最初に追加します。
builder.Services.AddResiliencePipeline("composed", pipelineBuilder =>
{
// 1. Total timeout (outermost -- caps entire operation)
pipelineBuilder.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(45)
});
// 2. Retry (retries on transient failures)
pipelineBuilder.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.Handle<TimeoutException>()
});
// 3. Circuit breaker (stops calling failing services)
pipelineBuilder.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.1,
MinimumThroughput = 20,
SamplingDuration = TimeSpan.FromSeconds(30),
BreakDuration = TimeSpan.FromSeconds(10),
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.Handle<TimeoutException>()
});
// 4. Attempt timeout (innermost -- caps single attempt)
pipelineBuilder.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(10)
});
});
型指定のレジリエンスパイプライン
結果を返す操作には、ResiliencePipeline<T> を使用します。
builder.Services.AddResiliencePipeline<string, HttpResponseMessage>(
"typed-http",
pipelineBuilder =>
{
pipelineBuilder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
});
});
ヘッジング戦略
並列リクエストを送信して、テールレイテンシを短縮します。ヘッジング戦略は、最初のリクエストの応答が遅い場合に、追加の試行をディスパッチします。
builder.Services
.AddHttpClient("search-api")
.AddStandardHedgingHandler(options =>
{
options.Hedging.MaxHedgedAttempts = 2;
options.Hedging.Delay = TimeSpan.FromMilliseconds(500);
// Hedging sends a parallel request if the first hasn't
// responded within 500ms
});
ヘッジングを使用する場合:
- 操作が冪等である場合(GET リクエスト、読み取り専用クエリ)
- 追加の負荷よりもテールレイテンシの短縮が重要な場合
ヘッジングを使用しない場合:
- 操作に副作用がある場合(POST、PUT、DELETE)
- 下流サービスが負荷の増加に対応できない場合
テレメトリと可観測性
DI 統合を使用すると、Polly v8 は標準で System.Diagnostics 経由のメトリクスとトレースを出力します。
組み込みメトリクス
Microsoft.Extensions.Resilience パッケージは、次の項目を自動的に報告します。
| メトリクス | 説明 |
|---|---|
polly.strategy.attempt.duration | 各試行の所要時間 |
polly.strategy.pipeline.duration | パイプライン実行全体の所要時間 |
polly.strategy.attempt.count | 試行回数(再試行を含む) |
これらは、アプリケーションで OpenTelemetry SDK を構成すると、自動的に OpenTelemetry と統合されます。コレクターの設定については、[skill:dotnet-observability] を参照してください。
テレメトリの有効化
DI ベースの登録(AddResiliencePipeline、AddStandardResilienceHandler)を使用すると、レジリエンスのテレメトリは自動的に有効になります。Microsoft.Extensions.Resilience パッケージは、DI から提供される標準の ILoggerFactory と IMeterFactory を通じて構造化ログとメトリクスを出力する MeteringEnricher と LoggingEnricher を登録します。
// Telemetry is automatic -- no extra configuration needed.
// Structured logs appear via ILogger; metrics via IMeter.
builder.Services
.AddHttpClient("catalog-api")
.AddStandardResilienceHandler();
// To see resilience logs, set the Polly category to Information:
// appsettings.json
// {
// "Logging": {
// "LogLevel": {
// "Polly": "Information"
// }
// }
// }
Microsoft.Extensions.Http.Polly からの移行
廃止予定の Microsoft.Extensions.Http.Polly パッケージからアップグレードする場合:
移行前(レガシー)
// Using Microsoft.Extensions.Http.Polly (superseded)
builder.Services
.AddHttpClient("catalog-api")
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, attempt =>
TimeSpan.FromSeconds(Math.Pow(2, attempt))))
.AddTransientHttpErrorPolicy(p =>
p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
移行後(最新)
// Using Microsoft.Extensions.Http.Resilience (current)
builder.Services
.AddHttpClient("catalog-api")
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(2);
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.CircuitBreaker.MinimumThroughput = 5;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
});
移行手順
Microsoft.Extensions.Http.Pollyの NuGet 参照をMicrosoft.Extensions.Http.Resilienceに置き換えますAddTransientHttpErrorPolicyの呼び出しをAddStandardResilienceHandlerまたはカスタムパイプラインに置き換えます- Polly v7 のポリシー設定を v8 の戦略オプションに変換します
IAsyncPolicy<HttpResponseMessage>型への明示的な参照を削除します- 再試行/サーキットブレーカーの動作が以前の設定と一致することを確認します
主な原則
- HTTP には標準パイプラインを使用します --
AddStandardResilienceHandler()は、十分に検証された既定値で最も一般的なケースに対応します - カスタムコードではなくオプションでカスタマイズします -- 標準ハンドラーのオプションで、再試行回数、バックオフ、サーキットブレーカーのしきい値、タイムアウトを設定できます
- HTTP 以外には
AddResiliencePipelineを使用します -- データベース呼び出し、メッセージキュー、ファイル I/O - 再試行には必ずジッターを追加します -- 複数のクライアントが同時に再試行する際のサンダリングハードを防ぎます
- appsettings.json で設定します -- 環境ごとに異なるしきい値が必要です。本番環境で値をハードコードしないでください
TimeoutRejectedExceptionを抑制しないでください -- 外側の戦略(再試行、サーキットブレーカー)が対応できるよう、パイプラインを通じて伝播させます- 新しいプロジェクトで
Microsoft.Extensions.Http.Pollyを使用しないでください -- Polly v7 をラップしており、後継パッケージに置き換えられています
エージェントで注意すべき点
- Polly v7 と v8 の API を混在させないでください -- v8 は
ResiliencePipelineと戦略オプションを使用し、v7 はIAsyncPolicyを使用します。両者は相互に置き換えられません。 AddStandardResilienceHandlerを二重に追加しないでください -- 完全なパイプラインを構成するため、二重に追加すると各戦略のレイヤーも二重になります。- 試行タイムアウトを全体のタイムアウトより長く設定しないでください -- 試行タイムアウトは全体のタイムアウトより短くする必要があります。そうしないと効果がありません。
- 冪等でない操作を再試行しないでください -- 安全に繰り返せる操作(GET、冪等性キーを使った冪等な書き込み)のみを再試行してください。
- 本番環境で再試行の遅延をハードコードしないでください -- 必ず
UseJitter = trueを設定し、遅延は appsettings で構成してください。