feat(infra): add Redis service and client for Phase 0 [FASE-0.1]

Add Redis 7 to Docker Compose with healthcheck and persistence, separate logical DBs for blacklist and cache, singleton redis client helpers, and unit tests with fakeredis.
This commit is contained in:
2026-05-18 13:18:45 +02:00
parent 821c4ac5ec
commit 9b70655b7e
6 changed files with 118 additions and 17 deletions

View File

@@ -1,17 +1,22 @@
"""Redis client singleton.
"""Redis client factories.
Provides a lazily-initialised Redis connection that is reused across
the application. The connection URL is read from ``settings.REDIS_URL``.
``settings.REDIS_URL`` selects the default logical database (usually ``0``).
Token blacklist and application cache use separate logical DBs on the same
Redis instance (``REDIS_TOKEN_BLACKLIST_DB``, ``REDIS_CACHE_DB``) so keys never
collide and TTL policies can differ per workload.
Usage::
from app.infrastructure.redis_client import get_redis
from app.infrastructure.redis_client import get_redis, get_redis_blacklist
r = get_redis()
r.set("key", "value", ex=300)
get_redis().set("key", "value", ex=300)
get_redis_blacklist().setex("blacklist:…", ttl, "1")
"""
from __future__ import annotations
import logging
from urllib.parse import urlparse, urlunparse
import redis
@@ -19,16 +24,43 @@ from app.config import settings
logger = logging.getLogger(__name__)
_redis_client: redis.Redis | None = None
_clients: dict[str, redis.Redis] = {}
def _redis_url_with_db(base_url: str, db_index: int) -> str:
"""Return *base_url* with its path replaced by ``/{db_index}``."""
parsed = urlparse(base_url)
path = f"/{db_index}"
return urlunparse(
(parsed.scheme, parsed.netloc, path, "", "", ""),
)
def _get_client(url: str) -> redis.Redis:
if url not in _clients:
_clients[url] = redis.from_url(url, decode_responses=True)
logger.info("Redis client connected to %s", url)
return _clients[url]
def get_redis() -> redis.Redis:
"""Return a shared Redis client, creating it on first call."""
global _redis_client
if _redis_client is None:
_redis_client = redis.from_url(
settings.REDIS_URL,
decode_responses=True,
)
logger.info("Redis client connected to %s", settings.REDIS_URL)
return _redis_client
"""Default Redis connection (URL from ``settings.REDIS_URL``)."""
return _get_client(settings.REDIS_URL)
def get_redis_blacklist() -> redis.Redis:
"""Redis DB used for JWT revocation (``jti`` keys with TTL)."""
url = _redis_url_with_db(
settings.REDIS_URL,
settings.REDIS_TOKEN_BLACKLIST_DB,
)
return _get_client(url)
def get_redis_cache() -> redis.Redis:
"""Redis DB reserved for shared cache (scores, queues, etc.)."""
url = _redis_url_with_db(
settings.REDIS_URL,
settings.REDIS_CACHE_DB,
)
return _get_client(url)