123 lines
4.1 KiB
Python
123 lines
4.1 KiB
Python
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",
|
||
]
|
||
|