Spaces:
Configuration error
Configuration error
| """ | |
| Redis-backed cache with a thread-safe in-memory fallback. | |
| Auto-initializes in-memory backend if connect() was not called (e.g. serverless environments). | |
| """ | |
| import json | |
| import asyncio | |
| import logging | |
| from typing import Any, Optional | |
| from datetime import datetime, timedelta | |
| logger = logging.getLogger(__name__) | |
| class _MemoryBackend: | |
| """Simple in-process TTL cache used when Redis is unavailable.""" | |
| def __init__(self): | |
| self._store: dict[str, tuple[Any, Optional[datetime]]] = {} | |
| self._lock = asyncio.Lock() | |
| async def get(self, key: str) -> Optional[str]: | |
| async with self._lock: | |
| entry = self._store.get(key) | |
| if entry is None: | |
| return None | |
| value, expires = entry | |
| if expires and datetime.utcnow() > expires: | |
| del self._store[key] | |
| return None | |
| return value | |
| async def set(self, key: str, value: str, ttl: Optional[int] = None): | |
| expires = datetime.utcnow() + timedelta(seconds=ttl) if ttl else None | |
| async with self._lock: | |
| self._store[key] = (value, expires) | |
| async def delete(self, key: str): | |
| async with self._lock: | |
| self._store.pop(key, None) | |
| async def close(self): | |
| pass | |
| class Cache: | |
| def __init__(self, redis_url: Optional[str] = None): | |
| self._redis_url = redis_url | |
| self._backend = None | |
| async def connect(self): | |
| if self._redis_url: | |
| try: | |
| import redis.asyncio as aioredis | |
| client = aioredis.from_url( | |
| self._redis_url, encoding="utf-8", decode_responses=True | |
| ) | |
| await client.ping() | |
| self._backend = client | |
| logger.info("Cache: connected to Redis at %s", self._redis_url) | |
| return | |
| except Exception as exc: | |
| logger.warning("Cache: Redis unavailable (%s). Using in-memory fallback.", exc) | |
| self._backend = _MemoryBackend() | |
| logger.info("Cache: using in-memory backend.") | |
| def _ensure_backend(self): | |
| if self._backend is None: | |
| self._backend = _MemoryBackend() | |
| async def get(self, key: str) -> Optional[Any]: | |
| self._ensure_backend() | |
| try: | |
| raw = await self._backend.get(key) | |
| if raw is None: | |
| return None | |
| try: | |
| return json.loads(raw) | |
| except (json.JSONDecodeError, TypeError): | |
| return raw | |
| except Exception as exc: | |
| logger.warning("Cache get error: %s", exc) | |
| return None | |
| async def set(self, key: str, value: Any, ttl: int = 300): | |
| self._ensure_backend() | |
| try: | |
| serialized = json.dumps(value) if not isinstance(value, str) else value | |
| await self._backend.set(key, serialized, ttl) | |
| except Exception as exc: | |
| logger.warning("Cache set error: %s", exc) | |
| async def delete(self, key: str): | |
| self._ensure_backend() | |
| try: | |
| await self._backend.delete(key) | |
| except Exception as exc: | |
| logger.warning("Cache delete error: %s", exc) | |
| async def close(self): | |
| if self._backend: | |
| try: | |
| await self._backend.close() | |
| except Exception: | |
| pass | |
| # Singleton used across the app | |
| cache: Cache = Cache() | |