| """Shared Redis connection for distributed rate limiting and prompt cache. |
| |
| Env-gated: when ``REDIS_URL`` is unset or Redis is unreachable, ``get_redis()`` |
| returns ``None`` and every caller falls back to its in-process implementation. |
| This keeps the app fully functional on a single instance and upgrades to |
| horizontally-correct shared state the moment a ``REDIS_URL`` is provided |
| (e.g. an Upstash free-tier database). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import logging |
| import threading |
| from typing import Any |
|
|
| from app.core.config import get_settings |
|
|
| logger = logging.getLogger(__name__) |
|
|
| _client: Any | None = None |
| _resolved = False |
| _lock = threading.Lock() |
|
|
|
|
| def get_redis() -> Any | None: |
| """Return a connected Redis client, or ``None`` if unavailable. |
| |
| Resolved once per process. A failed connection is cached as ``None`` so a |
| missing/broken Redis never adds per-request latency. |
| """ |
| global _client, _resolved |
| if _resolved: |
| return _client |
|
|
| with _lock: |
| if _resolved: |
| return _client |
| _resolved = True |
| settings = get_settings() |
| url = (getattr(settings, "redis_url", "") or "").strip() |
| if not url: |
| _client = None |
| return None |
| try: |
| import redis |
|
|
| client = redis.Redis.from_url( |
| url, |
| socket_connect_timeout=2, |
| socket_timeout=2, |
| decode_responses=True, |
| health_check_interval=30, |
| ) |
| client.ping() |
| _client = client |
| logger.info("Redis connected β distributed rate-limit + cache active.") |
| except Exception as exc: |
| _client = None |
| logger.warning("Redis unavailable (%s) β using in-memory fallback.", type(exc).__name__) |
| return _client |
|
|
|
|
| def redis_enabled() -> bool: |
| return get_redis() is not None |
|
|