# app/cache/redis_cache.py """ Redis cache wrapper — dengan graceful fallback ke in-memory """ import json import time from typing import Any, Optional from loguru import logger try: import redis.asyncio as redis REDIS_AVAILABLE = True except ImportError: REDIS_AVAILABLE = False class CacheLayer: """ Primary: Redis Fallback: in-memory dict (development / jika Redis down) """ def __init__(self, redis_url: Optional[str] = None): self._redis: Optional[redis.Redis] = None self._memory: dict[str, tuple[Any, float]] = {} # (value, expire_at) self._use_redis = False if REDIS_AVAILABLE and redis_url: try: self._redis = redis.from_url(redis_url) self._use_redis = True logger.info("Cache: Redis aktif") except Exception as e: logger.warning(f"Redis gagal, fallback ke memory: {e}") async def get(self, key: str) -> Optional[Any]: if self._use_redis: try: val = await self._redis.get(key) return json.loads(val) if val else None except Exception: pass # Memory fallback entry = self._memory.get(key) if entry: value, expire_at = entry if time.time() < expire_at: return value del self._memory[key] return None async def set( self, key: str, value: Any, ttl: int = 3600 ) -> None: serialized = json.dumps(value, default=str) if self._use_redis: try: await self._redis.setex(key, ttl, serialized) return except Exception: pass # Memory fallback self._memory[key] = (value, time.time() + ttl) async def delete(self, key: str) -> None: if self._use_redis: try: await self._redis.delete(key) return except Exception: pass self._memory.pop(key, None) def make_key(self, *parts: str) -> str: return ":".join(parts) # TTL Constants TTL = { "price": 300, # 5 menit "news": 1800, # 30 menit "sentiment": 1800, "prediction": 3600, # 1 jam "fundamental": 86400, # 24 jam } # Singleton _cache: Optional[CacheLayer] = None def get_cache(redis_url: Optional[str] = None) -> CacheLayer: global _cache if _cache is None: _cache = CacheLayer(redis_url) return _cache