mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 15:34:36 +01:00
zammad working
This commit is contained in:
322
backend/app/core/cache.py
Normal file
322
backend/app/core/cache.py
Normal file
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
Core Cache Service - Redis-based caching infrastructure
|
||||
Consolidates all caching functionality into core system infrastructure
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from datetime import datetime, timedelta
|
||||
import redis.asyncio as redis
|
||||
from redis.asyncio import Redis, ConnectionPool
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CoreCacheService:
|
||||
"""Core Redis-based cache service for system-wide caching"""
|
||||
|
||||
def __init__(self):
|
||||
self.redis_pool: Optional[ConnectionPool] = None
|
||||
self.redis_client: Optional[Redis] = None
|
||||
self.enabled = False
|
||||
self.stats = {
|
||||
"hits": 0,
|
||||
"misses": 0,
|
||||
"errors": 0,
|
||||
"total_requests": 0
|
||||
}
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the core cache service with connection pool"""
|
||||
try:
|
||||
# Create Redis connection pool for better resource management
|
||||
redis_url = getattr(settings, 'REDIS_URL', 'redis://localhost:6379/0')
|
||||
|
||||
self.redis_pool = ConnectionPool.from_url(
|
||||
redis_url,
|
||||
encoding="utf-8",
|
||||
decode_responses=True,
|
||||
socket_connect_timeout=5,
|
||||
socket_timeout=5,
|
||||
retry_on_timeout=True,
|
||||
max_connections=20, # Shared pool for all cache operations
|
||||
health_check_interval=30
|
||||
)
|
||||
|
||||
self.redis_client = Redis(connection_pool=self.redis_pool)
|
||||
|
||||
# Test connection
|
||||
await self.redis_client.ping()
|
||||
|
||||
self.enabled = True
|
||||
logger.info("Core cache service initialized with Redis connection pool")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize core cache service: {e}")
|
||||
self.enabled = False
|
||||
raise
|
||||
|
||||
async def cleanup(self):
|
||||
"""Cleanup cache resources"""
|
||||
if self.redis_client:
|
||||
await self.redis_client.close()
|
||||
self.redis_client = None
|
||||
|
||||
if self.redis_pool:
|
||||
await self.redis_pool.disconnect()
|
||||
self.redis_pool = None
|
||||
|
||||
self.enabled = False
|
||||
logger.info("Core cache service cleaned up")
|
||||
|
||||
def _get_cache_key(self, key: str, prefix: str = "core") -> str:
|
||||
"""Generate cache key with prefix"""
|
||||
return f"{prefix}:{key}"
|
||||
|
||||
async def get(self, key: str, default: Any = None, prefix: str = "core") -> Any:
|
||||
"""Get value from cache"""
|
||||
if not self.enabled:
|
||||
return default
|
||||
|
||||
try:
|
||||
cache_key = self._get_cache_key(key, prefix)
|
||||
value = await self.redis_client.get(cache_key)
|
||||
|
||||
if value is None:
|
||||
self.stats["misses"] += 1
|
||||
return default
|
||||
|
||||
self.stats["hits"] += 1
|
||||
self.stats["total_requests"] += 1
|
||||
|
||||
# Try to deserialize JSON
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return value
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cache get error for key {key}: {e}")
|
||||
self.stats["errors"] += 1
|
||||
return default
|
||||
|
||||
async def set(self, key: str, value: Any, ttl: Optional[int] = None, prefix: str = "core") -> bool:
|
||||
"""Set value in cache"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
try:
|
||||
cache_key = self._get_cache_key(key, prefix)
|
||||
ttl = ttl or 3600 # Default 1 hour TTL
|
||||
|
||||
# Serialize complex objects as JSON
|
||||
if isinstance(value, (dict, list, tuple)):
|
||||
value = json.dumps(value)
|
||||
|
||||
await self.redis_client.setex(cache_key, ttl, value)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cache set error for key {key}: {e}")
|
||||
self.stats["errors"] += 1
|
||||
return False
|
||||
|
||||
async def delete(self, key: str, prefix: str = "core") -> bool:
|
||||
"""Delete key from cache"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
try:
|
||||
cache_key = self._get_cache_key(key, prefix)
|
||||
result = await self.redis_client.delete(cache_key)
|
||||
return result > 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cache delete error for key {key}: {e}")
|
||||
self.stats["errors"] += 1
|
||||
return False
|
||||
|
||||
async def exists(self, key: str, prefix: str = "core") -> bool:
|
||||
"""Check if key exists in cache"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
try:
|
||||
cache_key = self._get_cache_key(key, prefix)
|
||||
return await self.redis_client.exists(cache_key) > 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cache exists error for key {key}: {e}")
|
||||
self.stats["errors"] += 1
|
||||
return False
|
||||
|
||||
async def clear_pattern(self, pattern: str, prefix: str = "core") -> int:
|
||||
"""Clear keys matching pattern"""
|
||||
if not self.enabled:
|
||||
return 0
|
||||
|
||||
try:
|
||||
cache_pattern = self._get_cache_key(pattern, prefix)
|
||||
keys = await self.redis_client.keys(cache_pattern)
|
||||
if keys:
|
||||
return await self.redis_client.delete(*keys)
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cache clear pattern error for pattern {pattern}: {e}")
|
||||
self.stats["errors"] += 1
|
||||
return 0
|
||||
|
||||
async def increment(self, key: str, amount: int = 1, ttl: Optional[int] = None, prefix: str = "core") -> int:
|
||||
"""Increment counter with optional TTL"""
|
||||
if not self.enabled:
|
||||
return 0
|
||||
|
||||
try:
|
||||
cache_key = self._get_cache_key(key, prefix)
|
||||
|
||||
# Use pipeline for atomic increment + expire
|
||||
async with self.redis_client.pipeline() as pipe:
|
||||
await pipe.incr(cache_key, amount)
|
||||
if ttl:
|
||||
await pipe.expire(cache_key, ttl)
|
||||
results = await pipe.execute()
|
||||
return results[0]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cache increment error for key {key}: {e}")
|
||||
self.stats["errors"] += 1
|
||||
return 0
|
||||
|
||||
async def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive cache statistics"""
|
||||
stats = self.stats.copy()
|
||||
|
||||
if self.enabled:
|
||||
try:
|
||||
info = await self.redis_client.info()
|
||||
stats.update({
|
||||
"redis_memory_used": info.get("used_memory_human", "N/A"),
|
||||
"redis_connected_clients": info.get("connected_clients", 0),
|
||||
"redis_total_commands": info.get("total_commands_processed", 0),
|
||||
"redis_keyspace_hits": info.get("keyspace_hits", 0),
|
||||
"redis_keyspace_misses": info.get("keyspace_misses", 0),
|
||||
"connection_pool_size": self.redis_pool.connection_pool_size if self.redis_pool else 0,
|
||||
"hit_rate": round(
|
||||
(stats["hits"] / stats["total_requests"]) * 100, 2
|
||||
) if stats["total_requests"] > 0 else 0,
|
||||
"enabled": True
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Redis stats: {e}")
|
||||
stats["enabled"] = False
|
||||
else:
|
||||
stats["enabled"] = False
|
||||
|
||||
return stats
|
||||
|
||||
@asynccontextmanager
|
||||
async def pipeline(self):
|
||||
"""Context manager for Redis pipeline operations"""
|
||||
if not self.enabled:
|
||||
yield None
|
||||
return
|
||||
|
||||
async with self.redis_client.pipeline() as pipe:
|
||||
yield pipe
|
||||
|
||||
# Specialized caching methods for common use cases
|
||||
|
||||
async def cache_api_key(self, key_prefix: str, api_key_data: Dict[str, Any], ttl: int = 300) -> bool:
|
||||
"""Cache API key data for authentication"""
|
||||
return await self.set(key_prefix, api_key_data, ttl, prefix="auth")
|
||||
|
||||
async def get_cached_api_key(self, key_prefix: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cached API key data"""
|
||||
return await self.get(key_prefix, prefix="auth")
|
||||
|
||||
async def invalidate_api_key(self, key_prefix: str) -> bool:
|
||||
"""Invalidate cached API key"""
|
||||
return await self.delete(key_prefix, prefix="auth")
|
||||
|
||||
async def cache_verification_result(self, api_key: str, key_prefix: str, key_hash: str, is_valid: bool, ttl: int = 300) -> bool:
|
||||
"""Cache API key verification result to avoid expensive bcrypt operations"""
|
||||
verification_data = {
|
||||
"key_hash": key_hash,
|
||||
"is_valid": is_valid,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
return await self.set(f"verify:{key_prefix}", verification_data, ttl, prefix="auth")
|
||||
|
||||
async def get_cached_verification(self, key_prefix: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cached verification result"""
|
||||
return await self.get(f"verify:{key_prefix}", prefix="auth")
|
||||
|
||||
async def cache_rate_limit(self, identifier: str, window_seconds: int, limit: int, current_count: int = 1) -> Dict[str, Any]:
|
||||
"""Cache and track rate limit state"""
|
||||
key = f"rate_limit:{identifier}:{window_seconds}"
|
||||
|
||||
try:
|
||||
# Use atomic increment with expiry
|
||||
count = await self.increment(key, current_count, window_seconds, prefix="rate")
|
||||
|
||||
remaining = max(0, limit - count)
|
||||
reset_time = int((datetime.utcnow() + timedelta(seconds=window_seconds)).timestamp())
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"limit": limit,
|
||||
"remaining": remaining,
|
||||
"reset_time": reset_time,
|
||||
"exceeded": count > limit
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Rate limit cache error: {e}")
|
||||
# Return permissive defaults on cache failure
|
||||
return {
|
||||
"count": 0,
|
||||
"limit": limit,
|
||||
"remaining": limit,
|
||||
"reset_time": int((datetime.utcnow() + timedelta(seconds=window_seconds)).timestamp()),
|
||||
"exceeded": False
|
||||
}
|
||||
|
||||
|
||||
# Global core cache service instance
|
||||
core_cache = CoreCacheService()
|
||||
|
||||
|
||||
# Convenience functions for backward compatibility and ease of use
|
||||
async def get(key: str, default: Any = None, prefix: str = "core") -> Any:
|
||||
"""Get value from core cache"""
|
||||
return await core_cache.get(key, default, prefix)
|
||||
|
||||
|
||||
async def set(key: str, value: Any, ttl: Optional[int] = None, prefix: str = "core") -> bool:
|
||||
"""Set value in core cache"""
|
||||
return await core_cache.set(key, value, ttl, prefix)
|
||||
|
||||
|
||||
async def delete(key: str, prefix: str = "core") -> bool:
|
||||
"""Delete key from core cache"""
|
||||
return await core_cache.delete(key, prefix)
|
||||
|
||||
|
||||
async def exists(key: str, prefix: str = "core") -> bool:
|
||||
"""Check if key exists in core cache"""
|
||||
return await core_cache.exists(key, prefix)
|
||||
|
||||
|
||||
async def clear_pattern(pattern: str, prefix: str = "core") -> int:
|
||||
"""Clear keys matching pattern from core cache"""
|
||||
return await core_cache.clear_pattern(pattern, prefix)
|
||||
|
||||
|
||||
async def get_stats() -> Dict[str, Any]:
|
||||
"""Get core cache statistics"""
|
||||
return await core_cache.get_stats()
|
||||
Reference in New Issue
Block a user