Agri-AI / backend /utils /cache.py
Akash-Dragon's picture
fix(backend): lazy-initialize cache backend to resolve FUNCTION_INVOCATION_FAILED on Vercel
b0e041f
Raw
History Blame Contribute Delete
3.43 kB
"""
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()