colibri.assistant.ai / utils /rate_limiter.py
Gouzi Mohaled
Ajoute des fichiers et sous-dossiers supplémentaires
fe4792e
raw
history blame
6.42 kB
import asyncio
import pytz
from datetime import datetime, timedelta
import time
from typing import Dict, Optional
import logging
from utils.logging_utils import log_to_file
from utils.admin import supabase # Ajoutez cet import avec les autres
from utils.cache_manager import CacheManager
cache_manager = CacheManager() # optimiser les statistiques d'utilisation avec le cache
class RateLimiter:
def __init__(self, requests_per_second: int = 10, burst_limit: int = 20): # 10 requetes par seconde, 20 requetes en rafale
"""
Initialise le rate limiter
requests_per_second: Nombre maximal de requêtes par seconde
burst_limit: Nombre maximal de requêtes en rafale
"""
self.rate = requests_per_second # Nombre maximal de requetes pas seconde (aujourd'hui)
self.burst_limit = burst_limit # Nombre maximal de requetes en rafale (aujourd'hui)
self.tokens = burst_limit # Nombre de jetons restants en rafale (aujourd'hui)
self.last_update = time.monotonic() # Derniere mise à jour
self.lock = asyncio.Lock() # Prise en charge du verrouillage
self._requests_count: Dict[str, int] = {} # Compteur de requetes
self.last_cleanup = time.time() # Date du dernier nettoyage
self.cleanup_interval = 3600 # 1 heure en secondes pour le nettoyage au bout d'une heure
self.cache_manager = CacheManager() # utilisation du cache pour stocker les statisqtiques d'utilisation (aujourd'hui)
async def acquire(self, user_id: Optional[str] = None) -> bool:
"""Tente d'acquérir un token pour faire une requête"""
async with self.lock:
now = time.monotonic()
time_passed = now - self.last_update
self.tokens = min(
self.burst_limit,
self.tokens + time_passed * self.rate
)
self.last_update = now
if self.tokens >= 1:
self.tokens -= 1
if user_id:
await self._track_request(user_id)
return True
log_to_file(f"Rate limit atteint pour l'utilisateur {user_id}", level=logging.WARNING)
return False
async def _get_stats_from_cache(self, user_id: str) -> Optional[Dict]:
"""Récupère les statistiques depuis le cache"""
cache_key = f"usage_stats:{user_id}"
return await cache_manager.get(cache_key)
async def _update_stats_cache(self, user_id: str, stats: Dict):
"""Met à jour les statistiques dans le cache"""
cache_key = f"usage_stats:{user_id}"
await cache_manager.set(cache_key, stats, expiry=timedelta(hours=1))
async def _track_request(self, user_id: str) -> None:
"""Suit les requêtes par utilisateur avec cache"""
try:
current_time = datetime.now(pytz.UTC).isoformat()
stats_data = None # Initialiser stats_data
# Vérifier d'abord le cache
cached_stats = await self._get_stats_from_cache(user_id)
if cached_stats:
# Mettre à jour les stats en cache
cached_stats['request_count'] += 1
cached_stats['last_request'] = current_time
await self._update_stats_cache(user_id, cached_stats)
stats_data = cached_stats
# Mettre à jour Supabase de manière asynchrone
supabase.table("usage_statistics").update({
"request_count": cached_stats['request_count'],
"last_request": current_time
}).eq("user_id", user_id).execute()
else:
# Vérifier Supabase
existing = supabase.table("usage_statistics").select("*").eq("user_id", user_id).execute()
if existing.data:
# Mettre à jour les stats existantes
stats = existing.data[0]
new_count = stats.get('request_count', 0) + 1
stats_data = {
"request_count": new_count,
"last_request": current_time
}
supabase.table("usage_statistics").update(stats_data).eq("user_id", user_id).execute()
await self._update_stats_cache(user_id, stats_data)
else:
# Créer nouvelles stats
stats_data = {
"user_id": user_id,
"request_count": 1,
"last_request": current_time,
"last_cleanup": current_time
}
supabase.table("usage_statistics").insert(stats_data).execute()
await self._update_stats_cache(user_id, stats_data)
if stats_data: # Vérifier que stats_data existe
self._requests_count[user_id] = stats_data['request_count']
log_to_file(f"Nouvelle requête pour l'utilisateur {user_id}. Total: {stats_data['request_count']}", level=logging.INFO)
except Exception as e:
log_to_file(f"Erreur lors du suivi des requêtes: {str(e)}", level=logging.ERROR)
# Continuer avec le comptage en mémoire en cas d'erreur
if user_id not in self._requests_count:
self._requests_count[user_id] = 1
else:
self._requests_count[user_id] += 1
async def get_user_requests_count(self, user_id: str) -> int:
"""Retourne le nombre de requêtes avec cache"""
try:
cached_stats = await self._get_stats_from_cache(user_id)
if cached_stats:
return cached_stats.get('request_count', 0)
response = supabase.table("usage_statistics").select("request_count").eq("user_id", user_id).execute()
if response.data:
count = response.data[0].get('request_count', 0)
await self._update_stats_cache(user_id, {"request_count": count})
return count
return 0
except Exception as e:
log_to_file(f"Erreur lors de la récupération du compte des requêtes: {str(e)}", level=logging.ERROR)
return self._requests_count.get(user_id, 0)