File size: 4,431 Bytes
ba0fd0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""
OmniDiag β€” Cache Initialisation & Helpers
==========================================
Wraps fastapi-cache2 with a Redis backend (production) or InMemoryBackend
(local dev / tests when REDIS_URL is not set).

Usage in main.py:
    from backend.cache import init_cache, cache_get, cache_set, cache_flush

    @asynccontextmanager
    async def lifespan(app):
        await init_cache()
        yield

    # Manual cache read/write (for POST endpoints where decorator won't work)
    cached = await cache_get(key)
    if cached is None:
        result = compute()
        await cache_set(key, result, ttl=300)

TTLs:
    Schema responses  β†’ 86400 s  (24 h) β€” changes only on deployment
    Predict responses β†’   300 s  ( 5 m) β€” short enough to stay fresh
"""

import hashlib
import json
import logging
import os
from typing import Any, Optional

log = logging.getLogger("omnidiag.cache")

# Will be set by init_cache()
_backend = None
_PREFIX = "omnidiag"


async def init_cache() -> None:
    """
    Initialise the cache backend.

    - If REDIS_URL is set β†’ RedisBackend (production / docker-compose)
    - Otherwise            β†’ InMemoryBackend (local dev & tests)
    """
    global _backend

    from fastapi_cache import FastAPICache
    from fastapi_cache.backends.inmemory import InMemoryBackend

    redis_url = os.getenv("REDIS_URL", "")
    if redis_url:
        try:
            import redis.asyncio as aioredis
            from fastapi_cache.backends.redis import RedisBackend

            client = aioredis.from_url(redis_url, encoding="utf-8", decode_responses=False)
            await client.ping()
            _backend = RedisBackend(client)
            FastAPICache.init(_backend, prefix=_PREFIX)
            log.info("Cache: RedisBackend initialised at %s", redis_url)
        except Exception as exc:
            log.warning("Cache: Redis unavailable (%s) β€” falling back to InMemoryBackend", exc)
            _backend = InMemoryBackend()
            FastAPICache.init(_backend, prefix=_PREFIX)
    else:
        _backend = InMemoryBackend()
        FastAPICache.init(_backend, prefix=_PREFIX)
        log.info("Cache: InMemoryBackend initialised (set REDIS_URL for Redis)")


def _make_key(*parts: str) -> str:
    return f"{_PREFIX}:" + ":".join(parts)


def predict_cache_key(disease: str, patient_data: dict) -> str:
    """Deterministic cache key for a predict request."""
    fingerprint = hashlib.sha256(
        json.dumps({"disease": disease, "data": patient_data}, sort_keys=True).encode()
    ).hexdigest()[:16]
    return _make_key("predict", disease, fingerprint)


def schema_cache_key(disease: str) -> str:
    return _make_key("schema", disease)


async def cache_get(key: str) -> Optional[Any]:
    """Return the cached value for *key*, or None on miss / error."""
    if _backend is None:
        return None
    try:
        from fastapi_cache import FastAPICache
        value = await FastAPICache.get_backend().get(key)
        if value is None:
            return None
        return json.loads(value)
    except Exception as exc:
        log.debug("cache_get error for key=%s: %s", key, exc)
        return None


async def cache_set(key: str, value: Any, ttl: int = 300) -> None:
    """Store *value* under *key* with a TTL in seconds."""
    if _backend is None:
        return
    try:
        from fastapi_cache import FastAPICache
        await FastAPICache.get_backend().set(key, json.dumps(value), ttl)
    except Exception as exc:
        log.debug("cache_set error for key=%s: %s", key, exc)


async def cache_flush() -> int:
    """
    Clear all keys with the OmniDiag prefix.

    Returns the number of keys deleted (best-effort; -1 if count unavailable).
    """
    if _backend is None:
        return 0
    try:
        from fastapi_cache import FastAPICache
        backend = FastAPICache.get_backend()

        # RedisBackend exposes the raw client
        if hasattr(backend, "redis"):
            keys = await backend.redis.keys(f"{_PREFIX}:*")
            if keys:
                await backend.redis.delete(*keys)
            return len(keys)

        # InMemoryBackend: clear the internal store
        if hasattr(backend, "_store"):
            count = len(backend._store)
            backend._store.clear()
            return count

        return -1
    except Exception as exc:
        log.warning("cache_flush error: %s", exc)
        return -1