active: true
derived: false
groups:
- LIMIT
header: |
  堅牢なレートリミッターの実装
level: 1.2
links:
- ARCH008: 3P_GwS0JFM40QovY318yJfHonSHWqQiSUXfnIrPXI7w=
- REQ008: JwW37tcsNK-kOTVFLlc_fNR7sIDjO4ES68i3H_55w20=
normative: true
ref: ''
reviewed: psl6Di_xvBHcqLyM-efXOrFus2IGYJ0WHSaFLebgQK0=
status: proposed
text: |
  # スムーズなレートリミッター（GCRA）

  ## コンテキストと課題

  `beautyspot` のレート制限機能 (`limiter`) において、以下の課題があった。

  1.  **Idle Burst:** トークンバケット方式では、アイドル中にトークンが溜まり、再開直後にバースト（集中アクセス）が発生してしまう。これを防ぐために容量(`capacity`)を小さくすると、今度は巨大なコストを持つタスクが実行できなくなる（デッドロック）問題が発生する。
  2.  **Start Dash Prevention:** プロセス起動直後に複数のタスクが同時に走るのを防ぎたいが、最初の1回目まで待たされるのは避けたい。
  3.  **Clock Dependency:** システム時刻の変更に堅牢である必要がある。
  4.  **Max Cost Guard:** `tokens_per_minute` を単発タスクのコスト上限とする。
      * これを超えるコストが `consume()` に渡された場合、即座に `ValueError` を送出する。
      * 理由: APIの物理的なレート制限を超えるリクエストは、待機したところで成功する見込みが薄く、早期に設定ミスや入力ミスをユーザーに通知すべきであるため。

  ## 判断基準

  * **Burst Prevention**: 長時間のアイドル後に大量のタスクが集中するのを防ぎ、一定のレートを維持すること。
  * **Deadlock Freedom**: レート制限設定値（TPM）以下のコストであれば、どんなに大きなコストのタスクでも（待機時間を経て）実行可能であること。
  * **Zero Initial Latency**: 最初のタスクは待機時間なしで即座に開始できること。
  * **Fail Fast**: システムの設定（TPM）で物理的に不可能なリクエストは、実行前に即座にエラーにすること。

  ## 検討した選択肢

  * **Option 1**: トークンバケット（既存実装）。バーストしやすく、容量設定によってはデッドロックのリスクがある。
  * **Option 2**: GCRA (Generic Cell Rate Algorithm)。別名 "Leaky Bucket as a Meter"。定速性が高く、貯金を許さないためバーストに強い。

  ## 決定事項

  採用した選択肢: **Option 2**.

  アルゴリズムをトークンバケットから **GCRA (Generic Cell Rate Algorithm)** に変更する。

  * **理論的到達時刻 (TAT: Theoretical Arrival Time)** を管理する。
  * タスクが来ると、そのコスト分だけ TAT を未来へ進める。
  * `現在時刻 < TAT` ならば、その差分だけ待機（sleep）してから実行する。
  * `現在時刻 >= TAT` （アイドル状態）ならば、TAT を現在時刻にリセットしてから計算する（過去の貯金は捨てる）。

  ## 影響・結果

  * **ポジティブ**:
      * **完全な定速運転:** アイドル後でもバーストせず、設定されたレート（TPM）を厳守する。
      * **デッドロックフリー:** どんなに大きなコストのタスクでも、必要な待機時間を計算して処理できる。
      * **即時起動:** 最初の1回のリクエストは待ち時間なしで実行され、2回目以降からペーシングが効く。
      * **Fail Fast:** 設定値を超える無茶なリクエストを早期発見できる。
  * **ネガティブ**:
      * 実装が「トークンを消費する」という直感的なメタファーから少し離れる（コード内の説明で補足する）。
