121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
import time
|
|
import logging
|
|
import threading
|
|
from typing import Any, Dict, Optional
|
|
|
|
logger = logging.getLogger("ifixkart.cache")
|
|
|
|
class MemoryCache:
|
|
"""Thread-safe in-process memory cache with TTL (Time To Live)."""
|
|
def __init__(self):
|
|
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
self._lock = threading.Lock()
|
|
|
|
def get(self, key: str) -> Optional[Any]:
|
|
with self._lock:
|
|
item = self._cache.get(key)
|
|
if not item:
|
|
return None
|
|
if item["expire_at"] is not None and time.time() > item["expire_at"]:
|
|
del self._cache[key]
|
|
return None
|
|
return item["value"]
|
|
|
|
def set(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
|
|
expire_at = (time.time() + ttl_seconds) if ttl_seconds is not None else None
|
|
with self._lock:
|
|
self._cache[key] = {
|
|
"value": value,
|
|
"expire_at": expire_at
|
|
}
|
|
|
|
def delete(self, key: str) -> None:
|
|
with self._lock:
|
|
if key in self._cache:
|
|
del self._cache[key]
|
|
|
|
def invalidate_prefix(self, prefix: str) -> None:
|
|
with self._lock:
|
|
keys_to_del = [k for k in self._cache.keys() if k.startswith(prefix)]
|
|
for k in keys_to_del:
|
|
del self._cache[k]
|
|
|
|
def clear(self) -> None:
|
|
with self._lock:
|
|
self._cache.clear()
|
|
|
|
class CacheManager:
|
|
"""Consolidated cache interface with Redis connection fallback."""
|
|
def __init__(self):
|
|
self.memory_cache = MemoryCache()
|
|
self.redis_client = None
|
|
self._try_init_redis()
|
|
|
|
def _try_init_redis(self):
|
|
try:
|
|
import redis
|
|
import os
|
|
redis_host = os.getenv("REDIS_HOST", "127.0.0.1")
|
|
redis_port = int(os.getenv("REDIS_PORT", 6379))
|
|
self.redis_client = redis.Redis(
|
|
host=redis_host,
|
|
port=redis_port,
|
|
db=0,
|
|
socket_connect_timeout=1,
|
|
decode_responses=False
|
|
)
|
|
self.redis_client.ping()
|
|
logger.info("CacheManager: Redis cache connected successfully.")
|
|
except Exception:
|
|
self.redis_client = None
|
|
logger.warning("CacheManager: Redis is offline or not installed. Falling back to In-Memory cache.")
|
|
|
|
def get(self, key: str) -> Optional[Any]:
|
|
if self.redis_client:
|
|
try:
|
|
import json
|
|
data = self.redis_client.get(key)
|
|
if data:
|
|
return json.loads(data.decode("utf-8"))
|
|
except Exception as e:
|
|
logger.error(f"Redis get failed: {e}")
|
|
return self.memory_cache.get(key)
|
|
|
|
def set(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
|
|
if self.redis_client:
|
|
try:
|
|
import json
|
|
serialized = json.dumps(value)
|
|
self.redis_client.set(key, serialized, ex=ttl_seconds)
|
|
return
|
|
except Exception as e:
|
|
logger.error(f"Redis set failed: {e}")
|
|
self.memory_cache.set(key, value, ttl_seconds)
|
|
|
|
def delete(self, key: str) -> None:
|
|
if self.redis_client:
|
|
try:
|
|
self.redis_client.delete(key)
|
|
except Exception as e:
|
|
logger.error(f"Redis delete failed: {e}")
|
|
self.memory_cache.delete(key)
|
|
|
|
def invalidate_prefix(self, prefix: str) -> None:
|
|
if self.redis_client:
|
|
try:
|
|
keys = self.redis_client.keys(f"{prefix}*")
|
|
if keys:
|
|
self.redis_client.delete(*keys)
|
|
except Exception as e:
|
|
logger.error(f"Redis prefix invalidation failed: {e}")
|
|
self.memory_cache.invalidate_prefix(prefix)
|
|
|
|
def clear(self) -> None:
|
|
if self.redis_client:
|
|
try:
|
|
self.redis_client.flushdb()
|
|
except Exception as e:
|
|
logger.error(f"Redis flushdb failed: {e}")
|
|
self.memory_cache.clear()
|
|
|
|
cache = CacheManager()
|