Closed32

tagpr で比較的高い頻度で 'Secondary rate limit' となる問題を解決したい

ピン留めされたアイテム
snakasnaka

問題

この Issue と同様の現象が割と頻繁に発生する(その日の初回実行時でも発生する)

https://github.com/Songmu/tagpr/issues/170

エラーメッセージ

403 You have exceeded a secondary rate limit. Please wait a few minutes before you try again.

直前になにも実行していないにも関わらず Rate Limit と言われることが多々ある

ピン留めされたアイテム
snakasnaka

調査のまとめ

わかったこと

  • Issue の Search を API で実行したときに該当エラーが発生している
  • GITHUB_TOKEN の生成元によって挙動が変わっていそう
    • 問題が出ているケース
      • secrets.GITHUB_TOKEN を使用している
    • 問題が出ないケース
      • private な GitHub App を用意し、actions/create-github-app-token を使って Token を生成している
  • git-pr-release でも同様の状況は発生していた

わかっていないこと

  • 他の類似ツールでは問題が出ていない?
    • 類似ツール
      • git-pr-release
  • secrets.GITHUB_TOKEN の制限なのか?
  • GitHub App や Personal Access Token を使う以外の解決方法はないのか?
snakasnaka

ローカルで試す

debug 用にログを追加し response のヘッダ情報を出力してみた

検証用リポジトリ をローカルにcloneして tagpr を実行してみる

:

::debug [Search.Issues] x-ratelimit-reset=1726714496
::debug [Search.Issues] x-ratelimit-resource=search
::debug [Search.Issues] x-github-request-id=E7A1:1BDD16:DAE272:E75AD0:66EB925B
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-remaining=26
::debug [Search.Issues] x-ratelimit-used=4
::debug [Search.Issues] x-ratelimit-reset=1726714496
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-remaining=25
::debug [Search.Issues] x-ratelimit-resource=search
::debug [Search.Issues] x-github-request-id=E7A1:1BDD16:DAE2BB:E75B20:66EB925F
::debug [Search.Issues] x-ratelimit-used=5

:

::debug [Search.Issues] x-ratelimit-remaining=24
::debug [Search.Issues] x-ratelimit-resource=search
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-github-request-id=E7AD:3D430D:DEF9BF:EB7383:66EB9273
::debug [Search.Issues] x-ratelimit-reset=1726714496
::debug [Search.Issues] x-ratelimit-used=6
::debug [Search.Issues] x-github-request-id=E7AD:3D430D:DEF9FC:EB73C2:66EB9275
::debug [Search.Issues] x-ratelimit-remaining=23
::debug [Search.Issues] x-ratelimit-used=7
::debug [Search.Issues] x-ratelimit-reset=1726714496
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-resource=search

:

::debug [Search.Issues] x-github-request-id=E7B9:3E9C50:DA4E64:E6C828:66EB9288
::debug [Search.Issues] x-ratelimit-used=1
::debug [Search.Issues] x-ratelimit-remaining=29
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-reset=1726714568
::debug [Search.Issues] x-ratelimit-resource=search
::debug [Search.Issues] x-ratelimit-used=2
::debug [Search.Issues] x-ratelimit-reset=1726714568
::debug [Search.Issues] x-github-request-id=E7B9:3E9C50:DA4EE4:E6C899:66EB928C
::debug [Search.Issues] x-ratelimit-remaining=28
::debug [Search.Issues] x-ratelimit-limit=30
::debug [Search.Issues] x-ratelimit-resource=search
snakasnaka

連続で実行してみたが、 x-ratelimit-remaining の数値は比較的すぐに回復している

x-ratelimit-remaining=26
x-ratelimit-remaining=25
x-ratelimit-remaining=24
x-ratelimit-remaining=23
x-ratelimit-remaining=29  <-- 回復している
x-ratelimit-remaining=28
snakasnaka
snakasnaka

公式の workflow と自分の workflow を比較する

GITHUB_TOKEN の取得方法が異なる

自分の workflow

          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

公式(tagpr) の workflow

    - name: Generate token
      id: generate_token
      uses: actions/create-github-app-token@v1
      with:
        app-id: ${{ secrets.APP_ID }}
        private-key: ${{ secrets.PRIVATE_KEY }}

    - name: tagpr
      run: |
        make install
        echo '::echo::on'
        tagpr
      env:
        GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
snakasnaka

create-github-app-token で Token を生成してみる

やること

  • Register new GitHub App
  • Store the App's ID in your repository environment variables (example: APP_ID)
  • Store the App's private key in your repository secrets (example: PRIVATE_KEY)

https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow

snakasnaka

workflow の中で create-github-app-token を呼び出し Token を生成する

diff --git a/.github/workflows/run_tagpr.yml b/.github/workflows/run_tagpr.yml
index f1e6ca8..3eb5c61 100644
--- a/.github/workflows/run_tagpr.yml
+++ b/.github/workflows/run_tagpr.yml
@@ -18,6 +18,12 @@ jobs:
         with:
           go-version: '1.21'
       - run: go install github.com/snaka/tagpr/cmd/tagpr@debug-20240919-1708
+      - name: Generate token
+        id: generate_token
+        uses: actions/create-github-app-token@v1
+        with:
+          app-id: ${{ secrets.TAGPR_APP_ID }}
+          private-key: ${{ secrets.TAGPR_PRIVATE_KEY }}
       - run: tagpr
         env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

snakasnaka

secrets.GITHUB_TOKEN 使ってたときに比べて、こちらのほうが安定している。
何度か実行してみても Secondary Rate Limit のエラーは出てこない。

snakasnaka

類似ツール: git-pr-release を調べる

https://github.com/x-motemen/git-pr-release

snakasnaka

git-pr-release を使っているProjectの workflow 実行ログ見ると同様のエラーが出ているのを見つけた

workflow log
/opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/response/raise_error.rb:14:in `on_complete': GET https://api.github.com/search/issues?q=repo%3Axxxxxxxxxxxxxxx+is%3Apr+is%3Aclosed+a703494: 403 - You have exceeded a secondary rate limit. Please wait a few minutes before you try again. If you reach out to GitHub Support for help, please include the request ID 4BC0:A2B2B:1E37741:375C2A4:669F762E. // See: https://docs.github.com/free-pro-team@latest/rest/overview/rate-limits-for-the-rest-api#about-secondary-rate-limits (Octokit::TooManyRequests)
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/middleware.rb:60:in `block in call'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/response.rb:42:in `on_complete'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/middleware.rb:59:in `call'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/middleware/follow_redirects.rb:73:in `perform_with_redirection'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/middleware/follow_redirects.rb:61:in `call'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/rack_builder.rb:152:in `build_response'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/connection.rb:444:in `run_request'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/faraday-2.10.0/lib/faraday/connection.rb:200:in `get'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/sawyer-0.9.2/lib/sawyer/agent.rb:99:in `call'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/connection.rb:156:in `request'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/connection.rb:84:in `paginate'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/client/search.rb:99:in `search'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/octokit-9.1.0/lib/octokit/client/search.rb:49:in `search_issues'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:145:in `search_issue_numbers'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:179:in `fetch_squash_merged_pr_numbers_from_github'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:101:in `fetch_merged_prs'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:46:in `start'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:12:in `start'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/exe/git-pr-release:5:in `<top (required)>'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/bin/git-pr-release:23:in `load'
	from /opt/hostedtoolcache/Ruby/3.0.1/x64/bin/git-pr-release:23:in `<main>'

backtrace 見た感じ issue に対する search を実行している箇所で発生しているので、同じ状況と思われる

from /opt/hostedtoolcache/Ruby/3.0.1/x64/lib/ruby/gems/3.0.0/gems/git-pr-release-2.2.0/lib/git/pr/release/cli.rb:145:in `search_issue_numbers'

しかし、workflow 自体は失敗していないので、リトライ機構が用意されているのかもしれない?

snakasnaka

Octokit では特に Retry してなさそう... と思ったら、 workflow 側で 以下のようにやっていて、エラーを無視して進めていた

git-pr-release --squashed || echo 'Done.'
snakasnaka

go-github-ratelimit の使い方を調べる

https://github.com/gofri/go-github-ratelimit

READMEより

  rateLimiter, err := github_ratelimit.NewRateLimitWaiterClient(nil)
  if err != nil {
    panic(err)
  }
  client := github.NewClient(rateLimiter).WithAuthToken("your personal access token")

RateLimitWaiterClientgithub.NewClient() の第一引数(httpClient) として渡す

snakasnaka

NewRateLimitWaiterClient(nil)

https://github.com/gofri/go-github-ratelimit/blob/9d9726540b07a6a728cc6beb4a28cdc8a3a8f91a/github_ratelimit/ratelimit.go#L31-L40

http.Client{ Transport: waiter } とは?

Gemini に訊いてみると以下のような回答

  • Transport フィールドは、 http.Client が実際に HTTP リクエストを送信する方法を制御するインターフェースです。
  • waiterSecondaryRateLimitWaiter 型であり、これはおそらく API の二次的なレート制限に対処するためのカスタムの http.RoundTripper 実装です。
  • http.RoundTripper インターフェースは、HTTP リクエストを処理し、レスポンスを返すためのメソッド RoundTrip を定義します。
snakasnaka

http.RoundTripper について

https://qiita.com/tutuming/items/6006e1d8cf94bc40f8e8

現状の tagpr では以下のように oauth2.ClientTransport として渡されるようになっている

https://github.com/snaka/tagpr/blob/a66711363331fe1719771bd33e2a312f4fa98330/github.go#L22-L23

前述のようにgithub_ratelimit.NewRateLimitWaiterClient() の第一引数が RoundTripper ( Transport プロパティ ) を受け取るようになっているので、そこに oauthClient を渡すと良さそう?

このスクラップは2024/09/24にクローズされました