mcpField
D
dotnet-resilience

dotnet-resilience

github.com/wshaddix開発8683 回インストール35 週間前に更新
リポジトリ

dotnet-resilience について

dotnet-resilienceは、Polly v8を使ったモダンなレジリエンスパターンを扱う.NET開発者向けのスキルです。HTTPと非HTTPのパイプライン、依存性注入、テレメトリー、旧HTTP Pollyパッケージからの移行を説明します。

主な機能

  1. 01HTTP標準レジリエンスパイプライン
  2. 02リトライ、タイムアウト、サーキットブレーカー
  3. 03Polly v8のカスタムパイプライン
  4. 04依存性注入によるパイプライン登録
  5. 05レジリエンスのメトリクスとログ
  6. 06旧HTTP Pollyからの移行

活用シーン

  1. 01HTTPクライアントにリトライとタイムアウトを設定
  2. 02データベース呼び出し向けのパイプライン構築
  3. 03サーキットブレーカーとリトライ設定の調整
  4. 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);
    });
移行手順
  1. Microsoft.Extensions.Http.Polly の NuGet 参照を Microsoft.Extensions.Http.Resilience に置き換えます
  2. AddTransientHttpErrorPolicy の呼び出しを AddStandardResilienceHandler またはカスタムパイプラインに置き換えます
  3. Polly v7 のポリシー設定を v8 の戦略オプションに変換します
  4. IAsyncPolicy<HttpResponseMessage> 型への明示的な参照を削除します
  5. 再試行/サーキットブレーカーの動作が以前の設定と一致することを確認します

主な原則

  • HTTP には標準パイプラインを使用します -- AddStandardResilienceHandler() は、十分に検証された既定値で最も一般的なケースに対応します
  • カスタムコードではなくオプションでカスタマイズします -- 標準ハンドラーのオプションで、再試行回数、バックオフ、サーキットブレーカーのしきい値、タイムアウトを設定できます
  • HTTP 以外には AddResiliencePipeline を使用します -- データベース呼び出し、メッセージキュー、ファイル I/O
  • 再試行には必ずジッターを追加します -- 複数のクライアントが同時に再試行する際のサンダリングハードを防ぎます
  • appsettings.json で設定します -- 環境ごとに異なるしきい値が必要です。本番環境で値をハードコードしないでください
  • TimeoutRejectedException を抑制しないでください -- 外側の戦略(再試行、サーキットブレーカー)が対応できるよう、パイプラインを通じて伝播させます
  • 新しいプロジェクトで Microsoft.Extensions.Http.Polly を使用しないでください -- Polly v7 をラップしており、後継パッケージに置き換えられています

エージェントで注意すべき点

  1. Polly v7 と v8 の API を混在させないでください -- v8 は ResiliencePipeline と戦略オプションを使用し、v7 は IAsyncPolicy を使用します。両者は相互に置き換えられません。
  2. AddStandardResilienceHandler を二重に追加しないでください -- 完全なパイプラインを構成するため、二重に追加すると各戦略のレイヤーも二重になります。
  3. 試行タイムアウトを全体のタイムアウトより長く設定しないでください -- 試行タイムアウトは全体のタイムアウトより短くする必要があります。そうしないと効果がありません。
  4. 冪等でない操作を再試行しないでください -- 安全に繰り返せる操作(GET、冪等性キーを使った冪等な書き込み)のみを再試行してください。
  5. 本番環境で再試行の遅延をハードコードしないでください -- 必ず UseJitter = true を設定し、遅延は appsettings で構成してください。

References

コメント