""" Cache utilities. Local-first behavior: use in-memory cache by default. Redis is only used when explicitly enabled via environment variables. """ import json import threading import time from typing import Any, Optional from app.config import CacheConfig from app.utils.logger import get_logger logger = get_logger(__name__) class MemoryCache: """In-memory cache (an alternative if Redis is unavailable)""" def __init__(self): self._cache = {} self._lock = threading.Lock() def get(self, key: str) -> Optional[str]: with self._lock: if key in self._cache: data, expiry = self._cache[key] if expiry > time.time(): return data else: del self._cache[key] return None def setex(self, key: str, ttl: int, value: str): with self._lock: expiry = time.time() + ttl self._cache[key] = (value, expiry) def delete(self, key: str): with self._lock: if key in self._cache: del self._cache[key] def clear(self): with self._lock: self._cache.clear() class CacheManager: """cache manager""" _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): if self._initialized: return self._initialized = True self._client = None self._use_redis = False # Local-first: do NOT touch Redis unless explicitly enabled. if not CacheConfig.ENABLED: self._client = MemoryCache() self._use_redis = False return # Try Redis only when enabled. try: import redis from app.config import RedisConfig self._client = redis.Redis( host=RedisConfig.HOST, port=RedisConfig.PORT, db=RedisConfig.DB, password=RedisConfig.PASSWORD, decode_responses=True, socket_connect_timeout=RedisConfig.CONNECT_TIMEOUT, socket_timeout=RedisConfig.SOCKET_TIMEOUT, ) self._client.ping() self._use_redis = True logger.info("Redis cache connected") except Exception as e: # Fall back silently (keep startup logs clean in local mode). logger.info(f"Redis is enabled but unavailable; using in-memory cache instead: {e}") self._client = MemoryCache() self._use_redis = False def get(self, key: str) -> Optional[Any]: """Get cache""" try: data = self._client.get(key) if data: return json.loads(data) return None except Exception as e: logger.error(f"Cache read failed: {e}") return None def set(self, key: str, value: Any, ttl: int = 300): """Set up cache""" try: self._client.setex(key, ttl, json.dumps(value)) except Exception as e: logger.error(f"Cache write failed: {e}") def delete(self, key: str): """Delete cache""" try: self._client.delete(key) except Exception as e: logger.error(f"Cache delete failed: {e}") @property def is_redis(self) -> bool: return self._use_redis