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
|