Spaces:
Sleeping
Sleeping
| # 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 |