google-api-nodejs-client で Calendar API をよんだら Rate Limit Exceeded (403)を踏んでリトライした話

## 概要
[google-api-nodejs-client](https://github.com/googleapis/google-api-nodejs-client)を使って、Calendar のAPIを叩くオレオレAPIを作ってみた際に、一定のアクセス量から、Rate Limit Exceeded を引いたため、それの原因っぽいものと、ある程度の緩和策について備忘録として記載しようかと思います。

- ※ Rate limit など、Google に関する API の仕様などは常に変動する可能性があります。
    - この記事の情報は2023/06/30時点の情報になります。

- ※ 当人は Node.js もgoogleのAPI についても初学者です🙇🏻‍♂️

## 何が起きた？
概要でも記載しましたが、[google-api-nodejs-client](https://github.com/googleapis/google-api-nodejs-client)を使って、Calendar のイベントを作るAPIを叩くオレオレAPIを作っていたのですが、APIの一定のアクセス量を超えたあたりで、下記のようなエラーが発生しました。

```
GaxiosError: Rate Limit Exceeded
    at Gaxios._request (/app/node_modules/gaxios/build/src/gaxios.js:130:23)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async JWT.requestAsync (/app/node_modules/google-auth-library/build/src/auth/oauth2client.js:382:18) {
  [stack]: 'Error: Rate Limit Exceeded\n' +
    '    at Gaxios._request (/app/node_modules/gaxios/build/src/gaxios.js:130:23)\n' +
    '    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n' +
    '    at async JWT.requestAsync (/app/node_modules/google-auth-library/build/src/auth/oauth2client.js:382:18)',
  [message]: 'Rate Limit Exceeded',
```

上記の最後にもありますが、エラーオブジェクトを更に見ると、下記のような箇所もあり、Rate limit にひっかかっていることがわかりました。

```
  code: 403,
  errors: [
    {
      domain: 'usageLimits',
      reason: 'rateLimitExceeded',
      message: 'Rate Limit Exceeded'
    },
    [length]: 1
  ]
```

## Google の Calendar のAPIのLate周りの仕様
[公式サイト](https://support.google.com/a/answer/2905486?hl=ja)を見ると下記のようでした。

> Google カレンダーのユーザー、管理者、組織を不正行為から保護するために、Google カレンダーには使用制限が設けられています
> 無料アカウントを有料アカウントに切り替えてから 60 日が経過すると、カレンダーの使用量の上限が上がり、より多くの操作が許容されるようになります。

また、下記のような記載もありましたが、今回の件で私は有料アカウントの情報で試していました。

> より厳しい制限が適用されるケース
> Google では、スパムからユーザーを保護するため、より厳密な制限を適用する正確な上限を公開していません。サブスクリプション アカウントは、次のアカウントよりも上限がはるかに高く設定されています。
> ・ お支払い基準額に達してから 60 日以内の有料サブスクリプション
> ・ 試用アカウント
> ・ 従来の無償版 G Suite のユーザー
> ・ Google for Nonprofits | 非営利団体向けプログラムのお客様

さらに公式を見ると下記の記載があります。

> 試用版の上限をサブスクリプション アカウントの上限に変更する
> 有料アカウントに切り替えても、使用量の上限が自動的に変更されることはありません。次の手順に沿って対応してください。
> 1. ドメインを購入します。
> 2. ドメインのコストを超える 100 米ドル以上の累計請求額を支払います。詳しくは、手動（早期）の支払いを行うをご覧ください。
> 3. 60 日待機します。
> その後、上限を引き上げることができます。

つまり、100米ドル以上の累計請求額を支払っていない限りは上限は撤廃されず、厳しい制限が適用されそうです。



### Rate Limit　の厳しい制限の具体的な値
厳しい制限とはどれほどのものかと、試しに実際に適当に負荷を適当にかけて検証してみると、 5req/sec 前後で発生しているようでした。

この点について、なにかドキュメントがないかと探してみるとフォーラムでは[古い回答](https://groups.google.com/g/google-apps-api-japan/c/qlws-vLbe_k/m/J1w3t5T7wWcJ)ですが、下記のように回答されていました。

> ドキュメント上の記載がありませんが、Calendar API(というか他のAPIも含め)にはユーザごとのAPIリクエスト数制限があるのでご注意下さい。
> Calendar APIではデフォルトで5リクエスト/秒間/ユーザです。

また、googleの[デベロッパーブログ](https://developers.googleblog.com/2021/06/the-google-calendar-api-has-changed-how-we-manage-API-usage.html)を見ると、下記の画像が添付されています。どうも画像を見る限り、 5req/sec に見えます。

![The Google Calendar API is changing how we manage API usage .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210923/c3a274eb-c68d-4957-3b15-2a974f9ca5dd.png)

## 対策について
では [google-api-nodejs-client]() での緩和策についてです。APIの用途次第で対応は変わるとは思いますが、私の場合は下記のような状況でした。

- あくまで検証なので 5req/sec は辛いが、最大でも20req/sec 程度に耐えれれば十分
- 処理はある程度遅延しても問題ない
- 制約上ユーザーを増やせばスケールするが、管理上ユーザーを増やすのは面倒

上記から、リトライする方針を一旦探してみました。

### google-api-nodejs-client のリトライ周り
まずはこちらのissue[Support for Retry for googleapis-node-js-client #1510](https://github.com/googleapis/google-api-nodejs-client/issues/1510)のissueが見つかりました。

> Greetings! We don't currently support retries by default. But that's something I'm happy to look into! in the interim, I suggest looking at something like p-retry.
> Actually - this is covered by #482! Closing this as a duplicate.

どうも上記を見ると対応していなさそうに見えましたが、 [#482](https://github.com/googleapis/google-api-nodejs-client/issues/482)のissueも確認してみると、[feat: retry requests by default #104](https://github.com/googleapis/nodejs-googleapis-common/pull/104) というプルリクが連携されていました。

では、プルリクを見てみると、リトライ問題を解決していそうです。

> BREAKING CHANGE: This change enables HTTP retries by default. The retry logic matches the defaults for gaxios:

> Fixes googleapis/google-api-nodejs-client#482.

また、2つのリポジトリの関連ですが、下記は一例ですがpackage-lock.jsonを見るに googleapis(リポジトリ名 =google-api-nodejs-client)はgoogleapis-common(リポジトリ名 = nodejs-googleapis-common)に依存していそうです。

```
    "node_modules/googleapis": {
    (略)
      "dependencies": {
        "google-auth-library": "^8.0.2",
        "googleapis-common": "^6.0.0"
      },
　　　　　　　(略)
    },
```

### 元からリトライ機構があったのになぜ動かない？
先程のnodejs-googleapis-commonの対応は2019年頃に入っているはずです。ではなぜ、リトライしなかったのでしょうか...。少し調べて見るに、問題はリトライのデフォルトオプションに問題がありました。

[gaxios](https://github.com/googleapis/gaxios)のREADME.mdにも記載がありますが、デバッグなどをしてみると下記の値が渡されていました。

```
    retryConfig: {
      currentRetryAttempt: 0,
      retry: 3,
      httpMethodsToRetry: [ 'GET', 'HEAD', 'PUT', 'OPTIONS', 'DELETE', [length]: 5 ],
      noResponseRetries: 2,
      statusCodesToRetry: [
        [ 100, 199, [length]: 2 ],
        [ 429, 429, [length]: 2 ],
        [ 500, 599, [length]: 2 ],
        [length]: 3
      ]
    }
```

私が叩いていたのは Calendar のイベントを "作る"(= POST) APIです。 httpMethodsToRetry に POST が含まれていないため、デフォルトではリトライしません。

また、Rate Limit Exceeded はHTTPステータスコードは 403 なので、statusCodesToRetry を見るにこれもリトライ対象ではありません。

## 実際に試す + 結果
下記のようにして、httpMethodsToRetry に POSTを追加して、statusCodesToRetryに403を含めたところ、リトライされ、エラーが発生しないことが確認できました。

※ 該当部だけ抜粋
```
const { google } = require('googleapis')

google.options({
  retryConfig: {
    currentRetryAttempt: 0,
    retry: 5,
    httpMethodsToRetry: ['GET', 'HEAD', 'POST', 'PUT', 'OPTIONS', 'DELETE'],
    statusCodesToRetry: [
      [100, 199],
      [403, 403],
      [429, 429],
      [500, 599]
    ],
    retryDelay: 500
  }
})
```

また、 statusCodesToRetry の指定についてですが、[403, 403]は冗長のようにも感じたのですが、下記のようにレンジ(=範囲)で指定するため、 単一のステータスコードを指定する場合は `[403, 403]` とする必要がありそうです。

```
  // If this wasn't in the list of status codes where we want
  // to automatically retry, return.
  const retryRanges = [
    // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
    // 1xx - Retry (Informational, request still processing)
    // 2xx - Do not retry (Success)
    // 3xx - Do not retry (Redirect)
    // 4xx - Do not retry (Client errors)
    // 429 - Retry ("Too Many Requests")
    // 5xx - Retry (Server errors)
    [100, 199],
    [429, 429],
    [500, 599],
  ];
  config.statusCodesToRetry = config.statusCodesToRetry || retryRanges;
```

参考: [gaxios/src/retry.ts](https://github.com/googleapis/gaxios/blob/cbb9d17384dd786f9631a805508fc36c8289fe40/src/retry.ts#L37-L51)