アプリが GitHub API のレート制限をどう処理するかをテストする

Tip

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

概要
目標: アプリが GitHub REST API のレート制限をどのように処理するかをテストする
時間: 15 分
Plugins:RateLimitingPlugin、 GenericRandomErrorPlugin、 RetryAfterPlugin
前提条件:開発プロキシを設定する

アプリは、GitHub API を呼び出します。 これはあなたのマシン上では動作しますが、CI ジョブや大規模な組織、あるいはアクセスが集中する日によってレート制限を超えてしまい、失敗し始めます。 実際の API に対してテストするには、レート制限を使い切ってから、最大 1 時間待ってからやり直す必要があります。 開発プロキシは、選択した制限と時間枠でGitHubレート制限をローカルでシミュレートします。

GitHub が返す内容を把握する

GitHubには、REST API の 2 種類のレート制限があります。

プライマリ レート制限では、 1 時間あたりに行う要求の数が上限になります。 たとえば、認証されていない要求の場合は 60、個人用アクセス トークンを使用する要求の場合は 5,000 などです。 各応答には、現在位置を示すヘッダーが含まれます:

Header Meaning
x-ratelimit-limit 1時間あたりの最大リクエスト件数
x-ratelimit-remaining 現在のウィンドウに残っている要求の数
x-ratelimit-reset ウィンドウがリセットされる時刻 (UTC エポック秒数)

プライマリ制限を超えると、GitHubは403が429に設定されたx-ratelimit-remainingまたは0を返します。 x-ratelimit-reset までは再試行しないでください。

二次レート制限は、同時リクエストの多発や短時間での大量のコンテンツ作成などの急増からGitHubを保護します。 1 を超えると、GitHub はセカンダリレート制限に関するメッセージとともに 403 または 429 を返します。 応答に retry-after ヘッダーがある場合は、その秒数だけ待機します。 それ以外の場合は、少なくとも 1 分待ってから、要求が失敗し続ける場合は待機時間を増やしてください。

GitHubでは、レートが制限されている間も要求を送信し続ける統合を禁止することがあります。 詳細については、GitHubドキュメントの REST API のレート制限を参照してください。

プライマリレート制限をシミュレート

RateLimitingPlugin を使用して要求をカウントし、GitHubのレート制限ヘッダーを返します。 1 時間待たずにテストするには、低い上限値と短いウィンドウを使用します。

ファイル: 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": "RateLimitingPlugin",
      "enabled": true,
      "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
      "configSection": "githubRateLimit"
    }
  ],
  "urlsToWatch": [
    "https://api.github.com/*"
  ],
  "githubRateLimit": {
    "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/ratelimitingplugin.schema.json",
    "headerLimit": "x-ratelimit-limit",
    "headerRemaining": "x-ratelimit-remaining",
    "headerReset": "x-ratelimit-reset",
    "resetFormat": "UtcEpochSeconds",
    "costPerRequest": 1,
    "rateLimit": 5,
    "resetTimeWindowSeconds": 60,
    "warningThresholdPercent": 0,
    "whenLimitExceeded": "Custom",
    "customResponseFile": "github-rate-limit-exceeded.json"
  }
}

Caution

構成ファイルのRetryAfterPluginの前にRateLimitingPluginを追加します。 後で追加した場合、RateLimitingPluginが確認する前に、RetryAfterPluginが要求を処理します。

カスタム応答ファイルで、プライマリ レート制限を超えたときに GitHub が返す応答を定義します。

ファイル: github-rate-limit-exceeded.json

{
  "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/ratelimitingplugin.customresponsefile.schema.json",
  "statusCode": 429,
  "headers": [
    {
      "name": "content-type",
      "value": "application/json; charset=utf-8"
    }
  ],
  "body": {
    "message": "API rate limit exceeded for user ID 1.",
    "documentation_url": "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api"
  }
}

Dev Proxy を起動し、アプリを実行します。

devproxy --config-file devproxyrc.json

開発プロキシは、各分の最初の 5 つの要求を GitHub に転送し、応答に x-ratelimit-* ヘッダーを付加します。 6 番目の要求から、Dev Proxy はレート制限応答を返し、x-ratelimit-remaining は 0 に設定され、x-ratelimit-reset はウィンドウの末尾に設定されます。 ウィンドウがリセットされる前にアプリで API が再度呼び出された場合、RetryAfterPlugin はそれを報告し、リクエストを制限します。

アプリが次の条件を満たしていることを確認してください:

  • x-ratelimit-remainingを読み取り、0に達する前に速度が低下します。
  • レート制限応答後に API の呼び出しを停止し、x-ratelimit-resetになるまで待機します。
  • 警告なしに失敗するのではなく、"GitHub レート制限に達し、14:05 に再試行する" など、ユーザーに何が起こっているかを通知します。

Note

GitHubは、レート制限を超えると403または429を返します。 アプリが 403 も処理することをテストするには、 statusCode を 403に変更します。 RetryAfterPluginは429応答のみを追跡するため、403後の早期再試行は報告されません。

Tip

開発プロキシは、シミュレートされた制限に達するまで要求をGitHubに転送します。 これらの要求は、実際のGitHubレート制限に対してカウントされます。

セカンダリ レート制限をシミュレートする

セカンダリ レート制限はバーストで発生し、 retry-after ヘッダーが含まれます。 GenericRandomErrorPlugin を使用してそれらをランダムに返します。

ファイル: 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": "githubSecondaryRateLimit"
    }
  ],
  "urlsToWatch": [
    "https://api.github.com/*"
  ],
  "githubSecondaryRateLimit": {
    "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/genericrandomerrorplugin.schema.json",
    "errorsFile": "github-secondary-rate-limit.json",
    "rate": 50,
    "retryAfterInSeconds": 60
  }
}

ファイル: github-secondary-rate-limit.json

{
  "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/genericrandomerrorplugin.errorsfile.schema.json",
  "errors": [
    {
      "request": {
        "url": "https://api.github.com/*"
      },
      "responses": [
        {
          "statusCode": 429,
          "headers": [
            {
              "name": "content-type",
              "value": "application/json; charset=utf-8"
            },
            {
              "name": "retry-after",
              "value": "@dynamic"
            }
          ],
          "body": {
            "message": "You have exceeded a secondary rate limit. Please wait a few minutes before you try again.",
            "documentation_url": "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api#about-secondary-rate-limits"
          }
        }
      ]
    }
  ]
}

開発プロキシを起動し、アプリを実行します。 アプリが api を再度呼び出す前に、 retry-after ヘッダーの秒数を待機していることを確認します。 そうでない場合は、RetryAfterPlugin がそれを報告します。

調整プラグインで Octokit を使用する場合は、 onRateLimit ハンドラーと onSecondaryRateLimit ハンドラーが実行され、期待した結果が返されることを確認してください。

次のステップ

詳細については、RateLimitingPlugin を参照してください。

参照