# backend/app/connectors/ratelimit.py — 토큰 버킷 + 429/Retry-After 지수 백오프 (phase-13) import threading import time import httpx class RateLimiter: """간단 토큰 버킷 + 429/Retry-After 지수 백오프.""" def __init__(self, rate: float = 4, per: float = 1.0, max_backoff: float = 32.0): self.rate, self.per = rate, per self.allowance = rate self.last = time.monotonic() self.max_backoff = max_backoff self._lock = threading.Lock() def acquire(self) -> None: with self._lock: now = time.monotonic() self.allowance += (now - self.last) * (self.rate / self.per) self.last = now if self.allowance > self.rate: self.allowance = self.rate if self.allowance < 1.0: time.sleep((1.0 - self.allowance) * (self.per / self.rate)) self.allowance = 0.0 else: self.allowance -= 1.0 def handle_response(self, r: "httpx.Response", attempt: int = 0) -> bool: """429 면 Retry-After(또는 지수 백오프)만큼 대기 후 True(재시도) 반환.""" if r.status_code == 429: retry = float(r.headers.get("Retry-After", min(2**attempt, self.max_backoff))) time.sleep(min(retry, self.max_backoff)) return True return False