You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
38 lines
1.4 KiB
Python
38 lines
1.4 KiB
Python
# 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
|