from pydantic.generics import GenericModel import os import redis class RedisSettings(GenericModel): REDIS_HOST: str = os.getenv("REDIS_HOST", "192.168.1.19") REDIS_PORT: int = int(os.getenv("REDIS_PORT", 6379)) REDIS_DB: int = int(os.getenv("REDIS_DB", 0)) REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD") or None @property def REDIS_URL(self) -> str: """Получить Redis URL для подключения""" if self.REDIS_PASSWORD: return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" else: return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" def test_connection(self) -> bool: """Протестировать подключение к Redis""" try: get_redis_client().ping() print(f"✓ Успешно подключено к Redis на {self.REDIS_HOST}:{self.REDIS_PORT}") return True except redis.ConnectionError as e: print(f"✗ Ошибка подключения к Redis: {e}") return False # Глобальная конфигурация _redis_settings = RedisSettings() # Единый connection pool на весь процесс (max_connections=20 — для FastAPI с несколькими воркерами) _pool = redis.ConnectionPool( host=_redis_settings.REDIS_HOST, port=_redis_settings.REDIS_PORT, db=_redis_settings.REDIS_DB, password=_redis_settings.REDIS_PASSWORD, decode_responses=True, max_connections=20, socket_connect_timeout=3, socket_timeout=3, retry_on_timeout=True, ) def get_redis_settings() -> RedisSettings: """Получить конфигурацию Redis""" return _redis_settings def get_redis_client() -> redis.Redis: """Получить клиент из общего connection pool (не создаёт новое TCP-соединение).""" return redis.Redis(connection_pool=_pool) # --------------------------------------------------------------------------- # Блэклист токенов # --------------------------------------------------------------------------- _BLACKLIST_PREFIX = "token_blacklist:" def blacklist_token(jti: str, ttl_seconds: int) -> None: """Добавить jti токена в блэклист на ttl_seconds секунд.""" try: client = get_redis_client() client.setex(f"{_BLACKLIST_PREFIX}{jti}", ttl_seconds, "1") except redis.RedisError: pass # Redis недоступен — не блокируем работу def is_token_blacklisted(jti: str) -> bool: """Проверить, отозван ли токен.""" try: client = get_redis_client() return client.exists(f"{_BLACKLIST_PREFIX}{jti}") > 0 except redis.RedisError: return False # при ошибке Redis не блокируем пользователей # --------------------------------------------------------------------------- # Кэш # --------------------------------------------------------------------------- _CACHE_PREFIX = "cache:" def cache_get(key: str) -> str | None: """Получить значение из кэша по ключу. Возвращает None если нет или Redis недоступен.""" try: client = get_redis_client() return client.get(f"{_CACHE_PREFIX}{key}") except redis.RedisError: return None def cache_set(key: str, value: str, ttl_seconds: int = 300) -> None: """Сохранить строку в кэш с TTL (по умолчанию 5 минут).""" try: client = get_redis_client() client.setex(f"{_CACHE_PREFIX}{key}", ttl_seconds, value) except redis.RedisError: pass def cache_delete(key: str) -> None: """Удалить ключ из кэша.""" try: client = get_redis_client() client.delete(f"{_CACHE_PREFIX}{key}") except redis.RedisError: pass __all__ = [ "RedisSettings", "get_redis_settings", "get_redis_client", "blacklist_token", "is_token_blacklisted", "cache_get", "cache_set", "cache_delete", ]