Microsoft.Extensions.Http.Resilience を使用する .NET アプリで再試行とタイムアウトをテストする

Tip

スロットリングは初めてですか? スロットリングとは何かと、その対処方法について学びましょう。

概要
目標:.NET HTTP 回復性ハンドラーが再試行し、バックオフし、予期したとおりにタイムアウトすることを確認します
時間: 15 分
プラグイン:GenericRandomErrorPlugin、 RetryAfterPlugin、 LatencyPlugin
前提条件:開発プロキシを設定し、.NET アプリでMicrosoft.Extensions.Http.Resilience を使用します

AddStandardResilienceHandler()にHttpClientを追加しました。 どのようにして動作していると分かりますか? 呼び出す API が任意のタイミングで失敗することはほとんどなく、HttpMessageHandler をモックする単体テストでは、テストしたい回復性パイプラインをスキップしてしまいます。

開発プロキシは、アプリと API の間に配置されます。 アプリにエラーと遅い応答が返されるため、アプリは変更されずに実行され、回復性ハンドラーは実際の HTTP 応答に反応します。 Dev Proxy の出力には、すべての試行が表示されます。

標準の回復性ハンドラーの動作

テストする前に、何を期待するかを知ってください。 既定のオプションでは、AddStandardResilienceHandler():

Behavior Default
再試行をオンにします HTTP 500 以上、408、429、HttpRequestException、TimeoutRejectedException
再試行回数 3、指数バックオフとジッターがあり、2 秒から開始する
Retry-After ヘッダー 尊重します。 ハンドラーは、API が指定した時間だけ待機します。
試行タイムアウト 1 回の試行につき 10 秒
合計タイムアウト 要求には 30 秒 (すべての再試行を含む)

戦略とその既定値の完全な一覧については、 標準の回復性ハンドラーの既定値に関するページを参照してください。

アプリをDev Proxy経由でルーティングする

.NETはシステム プロキシを使用するため、Dev Proxyを起動すると、コードを変更せずにアプリの要求がインターセプトされます。 詳細については、「.NET アプリケーションでの Dev Proxy の使用」を参照してください。

一時的なエラーをシミュレートする

ハンドラーが再試行するエラーを返して、API への要求を失敗させる Dev Proxy の構成を作成します。 この例では、 https://api.contoso.comを使用します。 これを、アプリが呼び出す API の URL に置き換えます。

ファイル: devproxyrc.json

{
  "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/rc.schema.json",
  "plugins": [
    {
      "name": "RetryAfterPlugin",
      "enabled": true,
      "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll"
    },
    {
      "name": "GenericRandomErrorPlugin",
      "enabled": true,
      "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
      "configSection": "transientErrors"
    }
  ],
  "urlsToWatch": [
    "https://api.contoso.com/*"
  ],
  "transientErrors": {
    "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/genericrandomerrorplugin.schema.json",
    "errorsFile": "transient-errors.json",
    "rate": 50
  }
}

Caution

構成ファイルのRetryAfterPluginの前にGenericRandomErrorPluginを追加します。 後で追加した場合、RetryAfterPlugin で確認できるようになる前に、GenericRandomErrorPlugin が要求を失敗させます。

エラー ファイルで、スロットリング応答と 2 つのサーバー エラーを定義します。 @dynamic値は、Retry-After ヘッダーを設定し、アプリが API を再度呼び出す前にその時間待機していることを確認するようにRetryAfterPluginに指示します。

ファイル: transient-errors.json

{
  "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/genericrandomerrorplugin.errorsfile.schema.json",
  "errors": [
    {
      "request": {
        "url": "https://api.contoso.com/*"
      },
      "responses": [
        {
          "statusCode": 429,
          "headers": [
            {
              "name": "Retry-After",
              "value": "@dynamic"
            }
          ]
        },
        {
          "statusCode": 500
        },
        {
          "statusCode": 503
        }
      ]
    }
  ]
}

開発プロキシを起動し、アプリを実行します。

devproxy --config-file devproxyrc.json

失敗率が 50% の場合、ほとんどの要求は 1 回または 2 回の再試行後に回復します。 Dev Proxy の出力で、次のことを確認してください:

  • 429 の応答が返された後、同じ URL への次の応答は Retry-After 時間後に送信されます。 Dev Proxy では、既定で 5 秒が使用されます。 アプリが API を早すぎるタイミングで呼び出した場合、RetryAfterPlugin はそれを報告し、要求を調整します。
  • アプリが送信する試行回数が、構成した回数を超えることはありません。
  • レコードを作成する POST など、再試行したくない要求は、1 回のみ送信されます。 それらを除外するには、options.Retry.DisableForUnsafeHttpMethods() または options.Retry.DisableFor(...) を呼び出します。

再試行がなくなった場合の動作をテストする

再試行をすることで、些細なエラーを検出しにくくなります。 また、API呼び出しが失敗し続ける場合にアプリが何を行うかも知る必要があります。 開発プロキシを、失敗率 100% で起動します:

devproxy --config-file devproxyrc.json --failure-rate 100

Dev Proxy では、要求ごとに 4 回の試行 (元の要求と 3 回の再試行) が表示されます。 最後の再試行の後、標準ハンドラーは例外をスローしません。 コードに対して、直近のエラー応答を返します。 アプリがそれをどのように処理しているかを確認します。 たとえば、 EnsureSuccessStatusCode() は HttpRequestExceptionをスローし、 GetStringAsync() もスローします。 クラッシュや一般的なエラーの表示ではなく、アプリに役立つメッセージが表示されているか、フォールバックしていることを確認します。

テストタイムアウト

処理に時間がかかる API は、ハンドラー内で別の処理経路を起動します。 これをテストするには、LatencyPlugin を使用して、10 秒の試行タイムアウトを超えて応答を遅らせます。

ファイル: devproxyrc.json

{
  "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/rc.schema.json",
  "plugins": [
    {
      "name": "LatencyPlugin",
      "enabled": true,
      "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
      "configSection": "slowApi"
    }
  ],
  "urlsToWatch": [
    "https://api.contoso.com/*"
  ],
  "slowApi": {
    "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/latencyplugin.schema.json",
    "minMs": 11000,
    "maxMs": 15000
  }
}

開発プロキシを起動し、アプリを実行します。 各試行には 10 秒を超える時間がかかるため、試行タイムアウトによって試行は取り消され、ハンドラーが再試行します。 30 秒経過すると、トータル タイムアウトにより要求がキャンセルされ、コードには TimeoutRejectedException が返されます。 アプリがそれをキャッチし、何が起こったかをユーザーに伝えることを確認します。

Tip

独自の回復性設定で同じシナリオをテストするには、 AddStandardResilienceHandler(options => ...) 呼び出しの値を変更し、同じ開発プロキシ構成をもう一度実行します。

次のステップ

任意の API でのスロットリングのシミュレーションの詳細をご覧ください。

参照