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

Tip

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

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

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 への要求をエラーで失敗させる開発プロキシ構成を作成します。 この例では、 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を追加します。 後で追加した場合、GenericRandomErrorPluginが確認できるようになる前に、RetryAfterPluginがリクエストを失敗させます。

エラー ファイルで、スロットリング応答と 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 でのスロットリングのシミュレーションの詳細をご覧ください。

参照