discordBot/utils/rate_limiter.py
deadzilla 29f2836cea feat: добавлено логирование во все модули
- utils/news.py: добавлен logger, логирование ошибок RSS
- utils/cat.py: добавлен logger, логирование ошибок TheCatAPI
- utils/pogoda.py: улучшены логи fallback/warning при ошибках API
- utils/rate_limiter.py: debug-логи при ожидании токенов
- commands/pg.py, news.py, cat.py, morning.py, status.py, stats.py: logger + логи ошибок и успешного выполнения команд
- console_commands/pogoda.py, news.py, cat.py, morning.py, status.py, stats.py: logger + логи выполнения
- bot.py: логи запуска/остановки, проверки конфигурации, маршрутизации консольных команд, f-строки -> %s формат
- ISSUES.md: снят флаг задачи по логированию
- все 243 теста пройдены
2026-06-12 18:58:33 +05:00

79 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Rate-limiter на основе токен-бакета для внешних API-вызовов.
Каждый API получает свой RateLimiter с настройками rate (токенов/сек)
и burst (максимальный размер бакета). Перед каждым запросом вызывается
async acquire(), который ждёт освобождения токена при необходимости.
"""
import asyncio
import logging
import os
import time
from typing import Final
logger = logging.getLogger(__name__)
class RateLimiter:
"""Токен-бакет: заполняется со скоростью rate токенов/сек, максимум burst."""
def __init__(self, rate: float, burst: int) -> None:
"""
Args:
rate: Скорость пополнения токенов (токенов в секунду).
burst: Максимальный размер бакета.
"""
self.rate: float = rate
self.burst: int = burst
self.tokens: float = float(burst)
self.lock: asyncio.Lock = asyncio.Lock()
self._last_refill: float = time.monotonic()
def _refill(self) -> None:
"""Пополнить токены за прошедшее время."""
now: float = time.monotonic()
elapsed: float = now - self._last_refill
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
self._last_refill = now
async def acquire(self, token: int = 1) -> None:
"""Забрать token токенов, ждать если их нет."""
wait_count = 0
while True:
async with self.lock:
self._refill()
if self.tokens >= token:
self.tokens -= token
if wait_count:
logger.debug("RateLimiter: ждал %d раз(а)", wait_count)
return
wait_count += 1
# Ждём достаточно времени для восстановления 1 токена
await asyncio.sleep(token / self.rate)
# --- Готовые лимитеры по API ---
# TheCatAPI: бесплатно, 1 req/sec, burst 3
_CAT_RATE: Final[float] = float(os.getenv("CAT_API_RATE", "1"))
_CAT_BURST: Final[int] = int(os.getenv("CAT_API_BURST", "3"))
# wttr.in: без ключа, 1 req/sec, burst 3
_WEATHER_RATE: Final[float] = float(os.getenv("WEATHER_API_RATE", "1"))
_WEATHER_BURST: Final[int] = int(os.getenv("WEATHER_API_BURST", "3"))
# Open-Meteo: fallback, 2 req/sec, burst 5
_OPEN_METEO_RATE: Final[float] = float(os.getenv("OPEN_METEO_API_RATE", "2"))
_OPEN_METEO_BURST: Final[int] = int(os.getenv("OPEN_METEO_API_BURST", "5"))
# Habr RSS: 1 req/sec, burst 2
_HABR_RSS_RATE: Final[float] = float(os.getenv("HABR_RSS_RATE", "1"))
_HABR_RSS_BURST: Final[int] = int(os.getenv("HABR_RSS_BURST", "2"))
# Экземпляры лимитеров
cat_limiter: RateLimiter = RateLimiter(_CAT_RATE, _CAT_BURST)
weather_limiter: RateLimiter = RateLimiter(_WEATHER_RATE, _WEATHER_BURST)
open_meteo_limiter: RateLimiter = RateLimiter(_OPEN_METEO_RATE, _OPEN_METEO_BURST)
habr_rss_limiter: RateLimiter = RateLimiter(_HABR_RSS_RATE, _HABR_RSS_BURST)