✓=レビュー済 ○=未レビュー ⚠=Suspect(複数同時表示あり。IDクリックで詳細へ)
| グループ | REQ | ARCH | SPEC | TST | IMPL | ADR |
|---|---|---|---|---|---|---|
| LIMIT | REQ008 ✓ 関数の実行頻度をトークンバケット方式で制限し、外部APIの呼び出しレートを制御できること。同期・非同期の両方に対応すること。 | ARCH008 ✓ ## コンポーネント構成 ```mermaid graph TD A["@spot.consume"] -->|宣言的適用| B[LimiterProto... | SPEC018 ✓ ## インターフェース ```python class TokenBucket(LimiterProtocol): def consume(self,... | TST018 ✓ ## 目的 `TokenBucket` はAPI呼び出しやリソースアクセスのレート制限を実現する。レートリミッターの不具合はAPIの過負荷(制限が甘い場合)や... | IMPL018 ✓ ## 実装概要 `LimiterProtocol` を実装する `TokenBucket` クラス。 GCRA (Generic Cell Rate Algo... | ADR003 ✓ # Smooth Rate Limiter (GCRA) ## Context and Problem Statement / コンテキスト `beauty... |
| LIMIT | REQ008 ✓ 関数の実行頻度をトークンバケット方式で制限し、外部APIの呼び出しレートを制御できること。同期・非同期の両方に対応すること。 | ARCH008 ✓ ## コンポーネント構成 ```mermaid graph TD A["@spot.consume"] -->|宣言的適用| B[LimiterProto... | SPEC018 ✓ ## インターフェース ```python class TokenBucket(LimiterProtocol): def consume(self,... | TST018 ✓ ## 目的 `TokenBucket` はAPI呼び出しやリソースアクセスのレート制限を実現する。レートリミッターの不具合はAPIの過負荷(制限が甘い場合)や... | IMPL018 ✓ ## 実装概要 `LimiterProtocol` を実装する `TokenBucket` クラス。 GCRA (Generic Cell Rate Algo... | ADR031 ✓ # Limiter Dependency Injection ## Context and Problem Statement / コンテキスト 以前、`c... |
| リンク方向 | カバー数 | カバー率 | 未カバー |
|---|---|---|---|
| ARCH → REQ | 1 / 1 | 100.0% | — |
| SPEC → ARCH | 1 / 1 | 100.0% | — |
| TST → SPEC | 1 / 1 | 100.0% | — |
| IMPL → SPEC | 1 / 1 | 100.0% | — |
| ADR → REQ | 1 / 1 | 100.0% | — |
関数の実行頻度をトークンバケット方式で制限し、外部APIの呼び出しレートを制御できること。同期・非同期の両方に対応すること。
親: —
graph TD
A["@spot.consume"] -->|宣言的適用| B[LimiterProtocol]
C[core.Spot] -->|DI| B
D[TokenBucket] --> B
D -->|アルゴリズム| E[GCRA]
D -->|時刻同期| F[monotonic clock]
| コンポーネント | 責務 | インターフェース |
|---|---|---|
| TokenBucket | GCRAアルゴリズムによるスムースなトラフィック制御 | consume(), consume_async() |
| @spot.consume | 関数実行前のトークン消費を透過的に行うデコレータ | cost (int or callable) |
| LimiterProtocol | レートリミッターの差し替えを可能にするインターフェース | LimiterProtocol |
sequenceDiagram
participant User as ユーザーコード
participant Dec as @spot.consume
participant Bucket as TokenBucket
User->>Dec: call func()
Dec->>Bucket: consume(cost)
Bucket->>Bucket: 次回実行許可時刻の計算 (GCRA)
alt 許可
Bucket-->>Dec: OK
Dec->>User: execute func()
else 拒否 (Over rate)
Bucket-->>Dec: raise ValueError/RateLimitError
end
| 技術領域 | 選定 | 理由 |
|---|---|---|
| アルゴリズム | GCRA (Generic Cell Rate Algorithm) | 固定ウィンドウと違い「スムースな」流量制御が可能 |
| 待機制御 | asyncio.sleep / time.sleep | 同期・非同期の両方のコンテキストで正確なスロットリングを実現 |
| コスト計算 | ダイナミックコスト | 引数に応じて消費トークン数を動的に変更可能 |
正確性: time.monotonic() を使用し、システム時刻の変更(NTP同期等)の影響を受けない
バースト制御: max_burst 設定により、アイドル後の過度なバーストを防止
スレッド安全: threading.Lock により、マルチスレッド環境での二重消費を防止
親: REQ008
子: SPEC018
class TokenBucket(LimiterProtocol):
def consume(self, cost: float = 1.0) -> None: ...
async def consume_async(self, cost: float = 1.0) -> None: ...
TAT - 許容バースト 以前の場合、リクエストを拒否または待機させる。consume: 待機が必要な場合は time.sleep() でブロック。consume_async: 待機が必要な場合は asyncio.sleep() で非占有待機。| 設定 | デフォルト | 説明 |
|---|---|---|
tokens_per_minute |
必須 | 1分間に許可する平均リクエスト数(コスト合計) |
max_burst |
1.0 |
許容されるバースト数(トークン単位) |
RateLimitError (ValueErrorのサブクラス) を送出する。max_burst を超えるコストを持つ単一リクエストは、常に拒否される。TAT は現在時刻より過去に一定以上離れないよう補正される(過度なバーストの防止)。親: ARCH008
TokenBucket はAPI呼び出しやリソースアクセスのレート制限を実現する。レートリミッターの不具合はAPIの過負荷(制限が甘い場合)やスループットの不必要な低下(制限が厳しすぎる場合)を招く。GCRA アルゴリズムのスムーズなレート制御を検証する。
references: tests/unit/test_limiter.py
親: SPEC018
子: —
LimiterProtocol を実装する TokenBucket クラス。
GCRA (Generic Cell Rate Algorithm) に基づき、スレッドセーフおよび
非同期対応のスムーズなレートリミッタを提供する。
内部状態として Theoretical Arrival Time (TAT) を保持し、
consume() で同期的スリープ、consume_async() で非同期的スリープを行う。
伝統的なトークンバケットとは異なり、長時間アイドル後に 一気にバーストを許容しない「Strict Pacing」を実現できる。 TATの更新と現在時刻の比較だけで待機時間を計算できるため、 メモリ効率と計算効率に優れる。
時刻の取得に time.monotonic() を使用し、システム時刻の変更(NTP同期など)の
影響を受けない堅牢な設計としている。
_consume_reservation() メソッドが待機時間の計算と TAT 更新を
threading.Lock でアトミックに行うcost > max_cost(バケット容量超過)の場合は、どれだけ待っても
処理できないため即座に ValueError を送出するtime.sleep()、非同期パスは asyncio.sleep() で待機するreferences: src/beautyspot/limiter.py
親: SPEC018
子: —
beautyspot のレート制限機能 (limiter) において、以下の課題があった。
capacity)を小さくすると、今度は巨大なコストを持つタスクが実行できなくなる(デッドロック)問題が発生する。tokens_per_minute を単発タスクのコスト上限とする。consume() に渡された場合、即座に ValueError を送出する。Chosen option: Option 2.
アルゴリズムをトークンバケットから GCRA (Generic Cell Rate Algorithm) に変更する。
現在時刻 < TAT ならば、その差分だけ待機(sleep)してから実行する。現在時刻 >= TAT (アイドル状態)ならば、TAT を現在時刻にリセットしてから計算する(過去の貯金は捨てる)。親: REQ008
子: —
以前、core.py の Spot クラスは TokenBucket 実装と密結合していました。Spot は tpm (tokens per minute) 整数引数を受け取り、内部で TokenBucket をインスタンス化していました。
この設計にはいくつかの制限がありました:
1. Testing: ユニットテストが TokenBucket 内の本物の time.sleep 呼び出しに依存するため、実行が遅くなっていました。
2. Extensibility: ユーザーがカスタムレートリミッター(例:Redisベースの分散リミッター)や異なるアルゴリズムを提供できませんでした。
3. Separation of Concerns: core.Spot がリミッターのライフサイクルと設定を管理しており、単一責任の原則に違反していました。
MockLimiter や NoOpLimiter を注入できるようにすること。core.Spot を簡素化し、リミッターの初期化処理を排除すること。Spot クラス内でリミッターの実装をハードコードする。LimiterProtocol を導入し、依存性注入 (DI) を使用してリミッターを外部から提供する。Chosen option: Option 2.
レートリミッターを core.Spot から切り離し、依存性注入 (DI) を使用します。
beautyspot.limiter に consume(cost: int) と consume_async(cost: int) メソッドを規定する LimiterProtocol を定義します。TokenBucket 実装は、型安全性と明確さのために LimiterProtocol を明示的に継承します。core.Spot.__init__ を、tpm の代わりに limiter: LimiterProtocol インスタンスを受け取るように変更します。__init__.py の Spot ファクトリ関数が、カスタムリミッターが提供されない場合のデフォルトの TokenBucket 作成を担当します。MockLimiter などを注入することで、テストの高速化が可能になった。Spot クラスの責務が削減され、コードが簡素化された。_Spot のシグネチャが変更されるため、直接インスタンス化していたコードに影響がある(公開されている Spot ファクトリ経由であれば影響なし)。親: REQ008
子: —