Spaces:
Sleeping
Sleeping
File size: 3,676 Bytes
1e732dd 696f787 1e732dd 696f787 fd5543a 1e732dd fd5543a 1e732dd fd5543a 1e732dd 696f787 fd5543a 1e732dd fd5543a 1e732dd fd5543a 1e732dd fd5543a 1e732dd fd5543a 1e732dd fd5543a 1e732dd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | """
MediGuard AI — Redis Cache
Exact-match caching with SHA-256 keys for RAG and analysis responses.
Gracefully degrades when Redis is unavailable.
"""
from __future__ import annotations
import hashlib
import json
import logging
from functools import lru_cache
from typing import Any
from src.settings import get_settings
logger = logging.getLogger(__name__)
try:
import redis as _redis
except ImportError: # pragma: no cover
_redis = None # type: ignore[assignment]
class RedisCache:
"""Thin Redis wrapper with SHA-256 key generation and JSON ser/de."""
def __init__(self, client: Any, default_ttl: int = 21600):
self._client = client
self._default_ttl = default_ttl
self._enabled = client is not None
@property
def enabled(self) -> bool:
return self._enabled
def ping(self) -> bool:
if not self._enabled:
return False
try:
return self._client.ping()
except Exception:
return False
@staticmethod
def _make_key(*parts: str) -> str:
raw = "|".join(parts)
return f"mediguard:{hashlib.sha256(raw.encode()).hexdigest()}"
def get(self, key: str) -> Any | None:
"""Get a cached value by key."""
if not self._enabled:
return None
cache_key = self._make_key(key)
try:
value = self._client.get(cache_key)
if value is None:
return None
return json.loads(value)
except Exception as exc:
logger.warning("Cache GET failed: %s", exc)
return None
def set(self, key: str, value: Any, *, ttl: int | None = None) -> bool:
"""Set a cached value with optional TTL."""
if not self._enabled:
return False
cache_key = self._make_key(key)
try:
self._client.setex(cache_key, ttl or self._default_ttl, json.dumps(value, default=str))
return True
except Exception as exc:
logger.warning("Cache SET failed: %s", exc)
return False
def delete(self, key: str) -> bool:
"""Delete a cached value by key."""
if not self._enabled:
return False
cache_key = self._make_key(key)
try:
self._client.delete(cache_key)
return True
except Exception as exc:
logger.warning("Cache DELETE failed: %s", exc)
return False
def flush(self) -> bool:
if not self._enabled:
return False
try:
self._client.flushdb()
return True
except Exception:
return False
class _NullCache(RedisCache):
"""No-op cache returned when Redis is disabled or unavailable."""
def __init__(self):
super().__init__(client=None)
@lru_cache(maxsize=1)
def make_redis_cache() -> RedisCache:
"""Factory — returns a live cache or a silent null-cache."""
settings = get_settings()
if not settings.redis.enabled or _redis is None:
logger.info("Redis caching disabled")
return _NullCache()
try:
client = _redis.Redis(
host=settings.redis.host,
port=settings.redis.port,
db=settings.redis.db,
decode_responses=True,
socket_connect_timeout=3,
)
client.ping()
logger.info("Redis connected (%s:%d)", settings.redis.host, settings.redis.port)
return RedisCache(client, settings.redis.ttl_seconds)
except Exception as exc:
logger.warning("Redis unavailable (%s), running without cache", exc)
return _NullCache()
|