264 lines
10 KiB
Python
264 lines
10 KiB
Python
"""
|
|
Comprehensive User Data Cache Service
|
|
Manages caching of expensive comprehensive user data operations.
|
|
"""
|
|
|
|
from typing import Dict, Any, Optional, Tuple
|
|
from datetime import datetime, timedelta
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import and_
|
|
from loguru import logger
|
|
import json
|
|
|
|
from models.comprehensive_user_data_cache import ComprehensiveUserDataCache
|
|
from services.calendar_generation_datasource_framework.data_processing.comprehensive_user_data import ComprehensiveUserDataProcessor
|
|
|
|
class ComprehensiveUserDataCacheService:
|
|
"""Service for caching comprehensive user data to improve performance."""
|
|
|
|
def __init__(self, db_session: Session):
|
|
self.db = db_session
|
|
self.data_processor = ComprehensiveUserDataProcessor()
|
|
|
|
async def get_cached_data(
|
|
self,
|
|
user_id: int,
|
|
strategy_id: Optional[int] = None,
|
|
force_refresh: bool = False,
|
|
**kwargs
|
|
) -> Tuple[Optional[Dict[str, Any]], bool]:
|
|
"""
|
|
Get comprehensive user data from cache or generate if not cached.
|
|
|
|
Args:
|
|
user_id: User ID
|
|
strategy_id: Optional strategy ID
|
|
force_refresh: Force refresh even if cached
|
|
**kwargs: Additional parameters for cache key generation
|
|
|
|
Returns:
|
|
Tuple of (data, is_cached)
|
|
"""
|
|
try:
|
|
# Generate cache key
|
|
data_hash = ComprehensiveUserDataCache.generate_data_hash(
|
|
user_id, strategy_id, **kwargs
|
|
)
|
|
|
|
if not force_refresh:
|
|
# Try to get from cache
|
|
cached_data = self._get_from_cache(user_id, strategy_id, data_hash)
|
|
if cached_data:
|
|
logger.info(f"✅ Cache HIT for user {user_id}, strategy {strategy_id}")
|
|
return cached_data, True
|
|
|
|
# Cache miss or force refresh - generate fresh data
|
|
logger.info(f"🔄 CACHE MISS - Tier: Database | User: {user_id} | Strategy: {strategy_id} | "
|
|
f"Force Refresh: {force_refresh} | Hash: {data_hash[:8]}... | Generating fresh data...")
|
|
fresh_data = await self.data_processor.get_comprehensive_user_data(user_id, strategy_id)
|
|
|
|
# Store in cache
|
|
self._store_in_cache(user_id, strategy_id, data_hash, fresh_data)
|
|
|
|
return fresh_data, False
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error in cache service: {str(e)}")
|
|
# Fallback to direct generation
|
|
try:
|
|
fallback_data = await self.data_processor.get_comprehensive_user_data(user_id, strategy_id)
|
|
return fallback_data, False
|
|
except Exception as fallback_error:
|
|
logger.error(f"❌ Fallback also failed: {str(fallback_error)}")
|
|
return None, False
|
|
|
|
async def get_comprehensive_user_data_backward_compatible(
|
|
self,
|
|
user_id: int,
|
|
strategy_id: Optional[int] = None,
|
|
force_refresh: bool = False,
|
|
**kwargs
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Backward-compatible method that returns data in the original format.
|
|
This prevents breaking changes for existing code.
|
|
|
|
Args:
|
|
user_id: User ID
|
|
strategy_id: Optional strategy ID
|
|
force_refresh: Force refresh even if cached
|
|
**kwargs: Additional parameters for cache key generation
|
|
|
|
Returns:
|
|
Dict containing comprehensive user data (original format)
|
|
"""
|
|
try:
|
|
data, is_cached = await self.get_cached_data(
|
|
user_id, strategy_id, force_refresh=force_refresh, **kwargs
|
|
)
|
|
|
|
if data:
|
|
# Return data in original format (without cache metadata)
|
|
return data
|
|
else:
|
|
# Fallback to direct processing if cache fails
|
|
logger.warning(f"Cache failed, using direct processing for user {user_id}")
|
|
return await self.data_processor.get_comprehensive_user_data(user_id, strategy_id)
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error in backward-compatible method: {str(e)}")
|
|
# Final fallback to direct processing
|
|
return await self.data_processor.get_comprehensive_user_data(user_id, strategy_id)
|
|
|
|
def _get_from_cache(
|
|
self,
|
|
user_id: int,
|
|
strategy_id: Optional[int],
|
|
data_hash: str
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""Get data from cache if valid."""
|
|
try:
|
|
# Query cache with conditions
|
|
cache_entry = self.db.query(ComprehensiveUserDataCache).filter(
|
|
and_(
|
|
ComprehensiveUserDataCache.user_id == user_id,
|
|
ComprehensiveUserDataCache.strategy_id == strategy_id,
|
|
ComprehensiveUserDataCache.data_hash == data_hash,
|
|
ComprehensiveUserDataCache.expires_at > datetime.utcnow()
|
|
)
|
|
).first()
|
|
|
|
if cache_entry:
|
|
# Calculate cache age and time to expiry
|
|
cache_age = datetime.utcnow() - cache_entry.created_at
|
|
time_to_expiry = cache_entry.expires_at - datetime.utcnow()
|
|
|
|
# Update access statistics
|
|
cache_entry.touch()
|
|
self.db.commit()
|
|
|
|
# Enhanced logging with metadata
|
|
logger.info(f"📊 CACHE HIT - Tier: Database | User: {user_id} | Strategy: {strategy_id} | "
|
|
f"Age: {cache_age.total_seconds():.1f}s | TTL: {time_to_expiry.total_seconds():.1f}s | "
|
|
f"Access Count: {cache_entry.access_count} | Hash: {data_hash[:8]}...")
|
|
|
|
return cache_entry.comprehensive_data
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error getting from cache: {str(e)}")
|
|
return None
|
|
|
|
def _store_in_cache(
|
|
self,
|
|
user_id: int,
|
|
strategy_id: Optional[int],
|
|
data_hash: str,
|
|
data: Dict[str, Any]
|
|
) -> bool:
|
|
"""Store data in cache."""
|
|
try:
|
|
# Remove existing cache entry if exists
|
|
self.db.query(ComprehensiveUserDataCache).filter(
|
|
and_(
|
|
ComprehensiveUserDataCache.user_id == user_id,
|
|
ComprehensiveUserDataCache.strategy_id == strategy_id,
|
|
ComprehensiveUserDataCache.data_hash == data_hash
|
|
)
|
|
).delete()
|
|
|
|
# Create new cache entry
|
|
cache_entry = ComprehensiveUserDataCache(
|
|
user_id=user_id,
|
|
strategy_id=strategy_id,
|
|
data_hash=data_hash,
|
|
comprehensive_data=data,
|
|
expires_at=ComprehensiveUserDataCache.get_default_expiry()
|
|
)
|
|
|
|
self.db.add(cache_entry)
|
|
self.db.commit()
|
|
|
|
logger.info(f"💾 CACHE STORED - Tier: Database | User: {user_id} | Strategy: {strategy_id} | "
|
|
f"Expires: {cache_entry.expires_at.strftime('%H:%M:%S')} | Hash: {data_hash[:8]}... | "
|
|
f"Data Size: {len(str(data))} chars")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error storing in cache: {str(e)}")
|
|
self.db.rollback()
|
|
return False
|
|
|
|
def invalidate_cache(self, user_id: int, strategy_id: Optional[int] = None) -> bool:
|
|
"""Invalidate cache for a user/strategy combination."""
|
|
try:
|
|
query = self.db.query(ComprehensiveUserDataCache).filter(
|
|
ComprehensiveUserDataCache.user_id == user_id
|
|
)
|
|
|
|
if strategy_id is not None:
|
|
query = query.filter(ComprehensiveUserDataCache.strategy_id == strategy_id)
|
|
|
|
deleted_count = query.delete()
|
|
self.db.commit()
|
|
|
|
logger.info(f"🗑️ Invalidated {deleted_count} cache entries for user {user_id}, strategy {strategy_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error invalidating cache: {str(e)}")
|
|
self.db.rollback()
|
|
return False
|
|
|
|
def cleanup_expired_cache(self) -> int:
|
|
"""Clean up expired cache entries."""
|
|
try:
|
|
deleted_count = self.db.query(ComprehensiveUserDataCache).filter(
|
|
ComprehensiveUserDataCache.expires_at <= datetime.utcnow()
|
|
).delete()
|
|
|
|
self.db.commit()
|
|
|
|
if deleted_count > 0:
|
|
logger.info(f"🧹 Cleaned up {deleted_count} expired cache entries")
|
|
|
|
return deleted_count
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error cleaning up cache: {str(e)}")
|
|
self.db.rollback()
|
|
return 0
|
|
|
|
def get_cache_stats(self) -> Dict[str, Any]:
|
|
"""Get cache statistics."""
|
|
try:
|
|
total_entries = self.db.query(ComprehensiveUserDataCache).count()
|
|
expired_entries = self.db.query(ComprehensiveUserDataCache).filter(
|
|
ComprehensiveUserDataCache.expires_at <= datetime.utcnow()
|
|
).count()
|
|
|
|
# Get most accessed entries
|
|
most_accessed = self.db.query(ComprehensiveUserDataCache).order_by(
|
|
ComprehensiveUserDataCache.access_count.desc()
|
|
).limit(5).all()
|
|
|
|
return {
|
|
"total_entries": total_entries,
|
|
"expired_entries": expired_entries,
|
|
"valid_entries": total_entries - expired_entries,
|
|
"most_accessed": [
|
|
{
|
|
"user_id": entry.user_id,
|
|
"strategy_id": entry.strategy_id,
|
|
"access_count": entry.access_count,
|
|
"last_accessed": entry.last_accessed.isoformat()
|
|
}
|
|
for entry in most_accessed
|
|
]
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Error getting cache stats: {str(e)}")
|
|
return {"error": str(e)}
|