notebook

都内でWEB系エンジニアやってます。

GitHubのGraphQLAPIのコストについて

プライベートでGitHubのGraphQL APIを使ってGitHub Project V2のデータを定期的に取得している

で、いつの間にか次のようなエラーが出るようになっていた

gh: API rate limit exceeded for user ID xxxxxxx.

ということでGitHubのGraphQL APIのリソース制限について調べたのでメモを残しておく

公式ドキュメント

リソースの制限事項 - GitHub Docs

docs.github.com

これ読んで理解できて対応できるならもう特にこの先読むことはなさそう

APIのレート制限が異なっているのはなぜでしょうか? GraphQL では、1 つの GraphQL 呼び出しで複数の REST 呼び出しを置き換えることができます。 単一の複雑なGraphQLの呼び出しが、数千のRESTリクエストと等価なこともあります。 単一の GraphQL 呼び出しは REST API レート制限を大幅に下回りますが、クエリはGitHub のサーバーが演算するのと同等の負荷になる可能性があります。 サーバーでのクエリのコストを正確に表すために、GraphQL API では呼び出しのレート制限スコアを正規化されたポイントのスケールに基づいて計算します。 クエリのスコアは、親のコネクションやその子のfirst及びlast引数を計算に入れます。

ということで、定義されたポイントをもとに制限している

で、今回はその制限にかかってエラーが起きてたという話

以下は「色々試してみた」内容なのであんまり…

コストがどのくらいかかっているのか確認する

これもドキュメントにあるが次のようなクエリを追加することでRateLimit関連の情報を得られる

  rateLimit {
    limit
    cost
    remaining
    resetAt
  }

とりあえずドキュメントのサンプルで試してみた結果から抜粋した

    "rateLimit": {
      "limit": 5000,
      "cost": 1,
      "remaining": 4965,
      "resetAt": "2023-06-05T01:29:00Z"
    }

最低コストは1らしいので1がコストとして消費された

実際に多くコスト消費しているクエリをみてみる

  • items_v2.graphql

これ

query($projectId: ID! $endCursor: String){
  node(id: $projectId) {
    ... on ProjectV2 {
      items(first: 50, after: $endCursor) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes{
          id
          createdAt
          updatedAt
          isArchived
          type
          fieldValues(first: 20) {
            nodes{
              ... on ProjectV2ItemFieldTextValue {
                text
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldDateValue {
                date
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldSingleSelectValue {
                name
                nameHTML
                optionId
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldIterationValue {
                iterationId
                startDate
                title
                titleHTML
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldLabelValue {
                labels(first: 5) {
                  nodes {
                    color
                    description
                    isDefault
                    name
                    url
                  }
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldMilestoneValue {
                milestone {
                  id
                  title
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldNumberValue {
                number
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldPullRequestValue {
                pullRequests(first: 10) {
                  nodes {
                    title
                    id
                    number
                    url
                    closed
                    closedAt
                    createdAt
                    merged
                    mergedAt
                    repository {
                      name
                    }
                    assignees(first: 5) {
                      nodes{
                        name
                        login
                      }
                    }
                    reviewRequests(first: 5) {
                      nodes {
                        requestedReviewer {
                          ... on User {
                            name
                          }
                        }
                      }
                    }
                  }
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldRepositoryValue {
                repository {
                  name
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldUserValue {
                users(first: 3) {
                  nodes {
                    id
                    login
                    name
                  }
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
            }
          }
          content{
            ... on DraftIssue {
              title
              body
              createdAt
            }
            ...on Issue {
              title
              id
              number
              url
              closed
              closedAt
              createdAt
              repository {
                name
              }
              milestone {
                id
                title
              }
              assignees(first: 5) {
                nodes{
                  name
                  login
                }
              }
            }
            ...on PullRequest {
              title
              id
              number
              url
              closed
              closedAt
              createdAt
              merged
              mergedAt
              repository {
                name
              }
              assignees(first: 5) {
                nodes{
                  name
                  login
                }
              }
              reviewRequests(first: 5) {
                nodes {
                  requestedReviewer {
                    ... on User {
                      name
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

query直下にrateLimitを入れて実行した結果

    "rateLimit": {
      "limit": 5000,
      "cost": 210,
      "remaining": 4545,
      "resetAt": "2023-06-05T01:29:00Z"
    }

一度の実行でコスト210も消費している

プロジェクトの都合上処理の流れとしてプロジェクトにひもづくすべてのItemを再帰的に取得していて、今回のケースだと24回目の実行時にエラーが出ていた…

210 * 24= 5040

1時間にコスト5000までのGraphQLリクエストが可能、5000を超えるリクエストを実行しようとした時点でエラー

消費コストを下げられる箇所を探す

コスト計算のおさらい

コスト = データを取得するためにする接続要求の合計/100

公式から具体例が示されている

  • 100 個のリポジトリが返されますが、API でリポジトリのリストを取得するために表示者のアカウントに接続する必要があるのは一度だけです。 したがって、リポジトリの要求 = 1 です
  • 50 個の Issue が返されますが、API では Issue のリストを取得するために 100 個のリポジトリそれぞれに接続する必要があります。 したがって、Issue の要求 = 100 です
  • 60 個のラベルが返されますが、API ではラベルのリストを取得するために潜在的に合計 5,000 個の Issue のそれぞれに接続する必要があります。 したがって、ラベルの要求 = 5,000 です
  • 合計 = 5,101 です

試しに、公式に載っているクエリをたたいてみたら次のようになった

    "rateLimit": {
      "limit": 5000,
      "cost": 51,
      "remaining": 4944,
      "resetAt": "2023-07-28T03:00:31Z"
    }

たしかにってところを確認した

実際のクエリで210ということは要求数でいうと21000くらいということになる

把握しやすくするため、とりあえず計算に影響しそうなところだけ残したクエリを準備

  • sample.graphql
query($projectId: ID! $endCursor: String){
  node(id: $projectId) {
    ... on ProjectV2 {
      items(first: 50, after: $endCursor) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes{
          id
          fieldValues(first: 20) {
            nodes{
              ... on ProjectV2ItemFieldLabelValue {
                labels(first: 5) {
                  nodes {
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldPullRequestValue {
                pullRequests(first: 10) {
                  nodes {
                    title
                    repository {
                      name
                    }
                    assignees(first: 5) {
                      nodes{
                        name
                      }
                    }
                    reviewRequests(first: 5) {
                      nodes {
                        requestedReviewer {
                          ... on User {
                            name
                          }
                        }
                      }
                    }
                  }
                }
              }
              ... on ProjectV2ItemFieldUserValue {
                users(first: 3) {
                  nodes {
                    id
                  }
                }
              }
            }
          }
          content{
            ... on DraftIssue {
              title
              body
            }
            ...on Issue {
              id
              repository {
                name
              }
              milestone {
                id
              }
              assignees(first: 5) {
                nodes{
                  name
                }
              }
            }
            ...on PullRequest {
              title
              id
              repository {
                name
              }
              assignees(first: 5) {
                nodes{
                  name
                }
              }
              reviewRequests(first: 5) {
                nodes {
                  requestedReviewer {
                    ... on User {
                      name
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  rateLimit {
    limit
    cost
    remaining
    resetAt
  }
}
  • 結果
    "rateLimit": {
      "limit": 5000,
      "cost": 210,
      "remaining": 4720,
      "resetAt": "2023-07-28T04:03:55Z"
    }

増減していないので大丈夫そう

削減箇所を決める

試しにitems以下で1つずつfirstの数値を下げて実行してみた結果

  • fieldValues
  • fieldValues.nodes.pullRequests

の2つのみしかコストの値に変化がなかった

ChatGPTくんに聞いたら「実際のコストは取得されるノードの実際の数によって異なり」とのこと、クエリの結果を鑑みると正しそうだが公式に明記されているのを見つけられなかった

実際に他の部分ではUserだったりAssigneeだったりと複数取ってこれるが実際は自分だけなのでほぼ1件しか返ってこないからかな

ということで変化のあった数値に関してどうするか決める

fieldValues

fieldValuesはカスタムで設定した列やデフォルトで含まれている列などの情報

GitHub Projectsの仕様で追加される、自身で追加するなどの事象が起きない限りは増えなそう

現状を確認し最大12個データが存在した

元の数値が20だったのでちょっと余裕もたせて15で設定した

158まで落ちた

fieldValues.nodes.pullRquests

今回のケースでは1つのItemに最大8個PullRequestをひもづけているItemがあったので最大8個とした

128まで落ちた

fieldValues.pullRequests.assigneesとfieldValues.pullRequests.reviewRequests

fieldValues.pullRequestsにひもづく、reviewRequestsを消したらかなり下がった

ここまで削減した数値の変化はいったんなしで試した例を載せている

fieldValues: 20
pr: 10
reviewRequests 1 -> 0
210 - 110

-100

assigneeを消しても同様

両方消したら10になったw

やっとわかってきた、これまでの例を見ると

PullRequestsのデータを取得するのに

50 * 20 / 100 = 10

PullRequestにひもづくデータを見に行く場合(assignee, reviewRequestsを指定する場合)

50 * 20 * 10 / 100 = 100

これが2つに対してコストが掛かる

なので210

他の部分のコストは1未満ということになりそう

content以下1つずつ消してもcontent自体を消しても特に変化なし

下位のNodeを見に行く必要があるのでそこまでの積算分要求が必要ということのよう

query($projectId: ID! $endCursor: String){
  node(id: $projectId) {
    ... on ProjectV2 {
      items(first: 50, after: $endCursor) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes{
          id
        }
      }
    }
  }
  rateLimit {
    limit
    cost
    remaining
    resetAt
  }
}

これだとコスト1

query($projectId: ID! $endCursor: String){
  node(id: $projectId) {
    ... on ProjectV2 {
      items(first: 50, after: $endCursor) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes{
          id
          fieldValues(first: 20) {
            nodes{
              ... on ProjectV2ItemFieldPullRequestValue {
                pullRequests(first: 10) {
                  nodes {
                    title
                    repository {
                      name
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  rateLimit {
    limit
    cost
    remaining
    resetAt
  }
}

これでコスト10

今回の利用ケースではPullRequestのAssigneeはいるけどreviewRequestsは不要と判断し片方だけ消すことにした

最終的なコストの内訳

最終的な内訳はこんな感じ

fieldValues.pullRequestsが必要なので
50(items) * 15(fieldValues) / 100 = 7.5

fieldValues.pullRequests.assigneesが必要なので
50(items) * 15(fieldValues) * 8(pullRequests) / 100 = 60

他はコスト1未満

小数点切り上げで68

という感じで最終的なコストは68となった

Actionsで本クエリを実行しているが、GitHub AppsのOAuthで委譲しているから自分のユーザーIDでリソース消費している

なのでPAT増やしたりしても1ユーザーにつき1時間5000のコストまでしかリクエストできない

他にも何かやったりしたらそれだけコスト消費するので消費コストを抑えられれば抑えられただけ良い

おわり

色々試してやっと「なんとなくわかった」という感じだが勉強にはなった