File size: 3,586 Bytes
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
"""
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, Dict, Optional

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_parts: str) -> Optional[Dict[str, Any]]:
        if not self._enabled:
            return None
        key = self._make_key(*key_parts)
        try:
            value = self._client.get(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, value: Dict[str, Any], *key_parts: str, ttl: Optional[int] = None) -> bool:
        if not self._enabled:
            return False
        key = self._make_key(*key_parts)
        try:
            self._client.setex(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_parts: str) -> bool:
        if not self._enabled:
            return False
        key = self._make_key(*key_parts)
        try:
            self._client.delete(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()