""" Redis helper — returns a connected client or None when Redis is unavailable. Key namespace design (prevents cross-file / cross-user data leakage): ann:{pdf_id}:{page_number}:{user_id} Every segment is mandatory, so: - User A opening file 1 never touches User A's data on file 2 - User A opening file 1 never touches User B's data on file 1 - Data for page 3 never affects page 7 TTL: 24 h — temp entry expires automatically if the user never returns. On permanent save the entry is deleted immediately. """ import logging import os from typing import Optional import redis as redis_lib logger = logging.getLogger(__name__) _client: Optional[redis_lib.Redis] = None _warned = False # log the "unavailable" warning only once def get_redis() -> Optional[redis_lib.Redis]: """Return a live Redis client, or None if Redis is unreachable.""" global _client, _warned if _client is not None: try: _client.ping() return _client except Exception: _client = None # connection dropped — try to reconnect below url = os.getenv("REDIS_URL", "redis://localhost:6379/0") try: r: redis_lib.Redis = redis_lib.from_url( url, decode_responses=True, socket_connect_timeout=2, socket_timeout=2, ) r.ping() _client = r _warned = False logger.info("Redis connected: %s", url) return _client except Exception as exc: if not _warned: logger.warning( "Redis unavailable (%s) — temp annotation cache disabled. " "Set REDIS_URL or start a local Redis instance to enable it.", exc, ) _warned = True return None # Key helpers — centralised so there's one place to change the format. ANN_TTL = 86_400 # 24 hours def ann_temp_key(pdf_id: int, page_number: int, user_id: int) -> str: """Fully-namespaced Redis key for one user's unsaved canvas on one PDF page.""" return f"ann:{pdf_id}:{page_number}:{user_id}" def ann_temp_page_pattern(pdf_id: int, page_number: int) -> str: """Glob pattern to list all users' temp entries for a given page.""" return f"ann:{pdf_id}:{page_number}:*"