""" API Router pentru managementul cache-ului """ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from typing import Optional, Dict, Any # import sys # Removed - no longer needed import os import time from datetime import datetime, timedelta from shared.auth.dependencies import get_current_user from shared.auth.models import CurrentUser from ..cache import get_cache, get_event_monitor, toggle_event_monitor router = APIRouter(tags=["cache"]) # Pydantic Models class CacheStatsResponse(BaseModel): """Răspuns statistici cache""" enabled: bool global_enabled: bool user_enabled: bool cache_type: str hit_rate: float total_hits: int total_misses: int queries_saved: Dict[str, int] response_times: Dict[str, Dict[str, Any]] cache_size: Dict[str, int] auto_invalidate: bool last_cleanup: Optional[str] = None class InvalidateCacheRequest(BaseModel): """Request pentru invalidare cache""" company_id: Optional[int] = None cache_type: Optional[str] = None class ToggleUserCacheRequest(BaseModel): """Request pentru toggle cache per-user""" enabled: bool class ToggleGlobalCacheRequest(BaseModel): """Request pentru toggle cache global""" enabled: bool class ToggleAutoInvalidateRequest(BaseModel): """Request pentru toggle auto-invalidation""" enabled: bool # Helper Functions async def _calculate_cache_stats() -> Dict[str, Any]: """Calculate comprehensive cache statistics""" cache = get_cache() if not cache: raise HTTPException(status_code=503, detail="Cache not initialized") # Get basic cache stats stats = await cache.get_stats() # Calculate hit rate memory_stats = stats.get('memory', {}) total_hits = memory_stats.get('hits', 0) total_misses = memory_stats.get('misses', 0) total_requests = total_hits + total_misses hit_rate = (total_hits / total_requests * 100) if total_requests > 0 else 0 # Calculate queries saved (from performance_log) queries_saved = await _calculate_queries_saved(cache) # Calculate response times per cache type response_times = await _calculate_response_times(cache) # Get cache sizes cache_size = { 'memory': memory_stats.get('size', 0), 'sqlite': stats.get('sqlite', {}).get('active_entries', 0) } # Get event monitor status monitor = get_event_monitor() auto_invalidate = monitor.running if monitor else False return { 'enabled': cache.config.enabled, 'global_enabled': cache.config.enabled, 'cache_type': cache.config.cache_type, 'hit_rate': round(hit_rate, 1), 'total_hits': total_hits, 'total_misses': total_misses, 'queries_saved': queries_saved, 'response_times': response_times, 'cache_size': cache_size, 'auto_invalidate': auto_invalidate, 'last_cleanup': None # TODO: track last cleanup time } async def _calculate_queries_saved(cache) -> Dict[str, int]: """Calculate queries saved by time period""" import aiosqlite try: async with aiosqlite.connect(cache.sqlite.db_path) as db: now = time.time() today_start = now - 86400 # 24 hours week_start = now - 604800 # 7 days # Today async with db.execute(""" SELECT COUNT(*) FROM performance_log WHERE cache_hit = 1 AND timestamp >= ? """, (today_start,)) as cursor: today = (await cursor.fetchone())[0] # This week async with db.execute(""" SELECT COUNT(*) FROM performance_log WHERE cache_hit = 1 AND timestamp >= ? """, (week_start,)) as cursor: week = (await cursor.fetchone())[0] # All time async with db.execute(""" SELECT COUNT(*) FROM performance_log WHERE cache_hit = 1 """) as cursor: total = (await cursor.fetchone())[0] return { 'today': today, 'week': week, 'total': total } except Exception as e: return {'today': 0, 'week': 0, 'total': 0} async def _calculate_response_times(cache) -> Dict[str, Dict[str, Any]]: """Calculate average response times per cache type""" import aiosqlite try: async with aiosqlite.connect(cache.sqlite.db_path) as db: # Get average times per cache type async with db.execute(""" SELECT cache_type, AVG(CASE WHEN cache_hit = 1 THEN response_time_ms ELSE NULL END) as avg_cached, AVG(CASE WHEN cache_hit = 0 THEN response_time_ms ELSE NULL END) as avg_oracle FROM performance_log WHERE timestamp >= ? GROUP BY cache_type """, (time.time() - 86400,)) as cursor: # Last 24 hours results = await cursor.fetchall() response_times = {} for row in results: cache_type, avg_cached, avg_oracle = row if avg_cached and avg_oracle: improvement = int((avg_oracle - avg_cached) / avg_oracle * 100) response_times[cache_type] = { 'cached': int(avg_cached), 'oracle': int(avg_oracle), 'improvement': improvement } return response_times except Exception as e: return {} # API Endpoints @router.get("/stats", response_model=CacheStatsResponse) async def get_cache_stats( current_user: CurrentUser = Depends(get_current_user) ): """ Obține statistici complete cache Returns: - Hit rate, queries saved, response times - Cache sizes (memory + SQLite) - Auto-invalidation status - Per-user cache setting """ try: cache = get_cache() if not cache: raise HTTPException(status_code=503, detail="Cache not initialized") # Get base stats stats = await _calculate_cache_stats() # Add user-specific setting user_enabled = await cache.is_enabled_for_user(current_user.username) stats['user_enabled'] = user_enabled return CacheStatsResponse(**stats) except Exception as e: raise HTTPException(status_code=500, detail=f"Error retrieving cache stats: {str(e)}") @router.post("/invalidate") async def invalidate_cache( request: InvalidateCacheRequest, current_user: CurrentUser = Depends(get_current_user) ): """ Invalidează cache Args: company_id: Opțional - invalidează doar pentru această companie cache_type: Opțional - invalidează doar acest tip de cache Returns: Message de confirmare """ try: cache = get_cache() if not cache: raise HTTPException(status_code=503, detail="Cache not initialized") await cache.invalidate( company_id=request.company_id, cache_type=request.cache_type ) if request.company_id and request.cache_type: message = f"Cache invalidated for company {request.company_id}, type {request.cache_type}" elif request.company_id: message = f"Cache invalidated for company {request.company_id}" elif request.cache_type: message = f"Cache invalidated for type {request.cache_type}" else: message = "All cache invalidated" return { "success": True, "message": message, "invalidated_at": datetime.now().isoformat() } except Exception as e: raise HTTPException(status_code=500, detail=f"Error invalidating cache: {str(e)}") @router.post("/toggle-user") async def toggle_user_cache( request: ToggleUserCacheRequest, current_user: CurrentUser = Depends(get_current_user) ): """ Toggle cache per-user Permite utilizatorului să activeze/dezactiveze cache-ul pentru el Folosit pentru A/B testing și comparații de performanță Args: enabled: True pentru activare, False pentru dezactivare Returns: Noul status """ try: cache = get_cache() if not cache: raise HTTPException(status_code=503, detail="Cache not initialized") await cache.set_user_cache_enabled(current_user.username, request.enabled) return { "success": True, "username": current_user.username, "cache_enabled": request.enabled, "message": f"Cache {'enabled' if request.enabled else 'disabled'} for user {current_user.username}" } except Exception as e: raise HTTPException(status_code=500, detail=f"Error toggling user cache: {str(e)}") @router.post("/toggle-global") async def toggle_global_cache( request: ToggleGlobalCacheRequest, current_user: CurrentUser = Depends(get_current_user) ): """ Toggle cache global (ADMIN only) Activează/dezactivează cache-ul la nivel global pentru toți utilizatorii Args: enabled: True pentru activare, False pentru dezactivare Returns: Noul status global """ try: # TODO: Add admin permission check # For now, allow any authenticated user cache = get_cache() if not cache: raise HTTPException(status_code=503, detail="Cache not initialized") # Update config (NOTE: This is runtime only, .env needs manual update) cache.config.enabled = request.enabled return { "success": True, "global_enabled": request.enabled, "message": f"Cache {'enabled' if request.enabled else 'disabled'} globally", "note": "This change is runtime only. Update .env CACHE_ENABLED for persistence." } except Exception as e: raise HTTPException(status_code=500, detail=f"Error toggling global cache: {str(e)}") @router.post("/toggle-auto-invalidate") async def toggle_auto_invalidation( request: ToggleAutoInvalidateRequest, current_user: CurrentUser = Depends(get_current_user) ): """ Toggle auto-invalidation monitoring Activează/dezactivează monitorizarea automată a {schema}.act pentru invalidarea cache-ului când se detectează modificări Args: enabled: True pentru activare, False pentru dezactivare Returns: Noul status auto-invalidation """ try: # TODO: Add admin permission check # For now, allow any authenticated user await toggle_event_monitor(request.enabled) return { "success": True, "auto_invalidate_enabled": request.enabled, "message": f"Auto-invalidation {'enabled' if request.enabled else 'disabled'}", "note": "Monitors max(id_act) in {schema}.act tables for changes" } except Exception as e: raise HTTPException(status_code=500, detail=f"Error toggling auto-invalidation: {str(e)}") @router.get("/health") async def cache_health(): """ Health check pentru sistemul de cache Returns: Status cache, mărime, și uptime """ try: cache = get_cache() if not cache: return { "status": "not_initialized", "enabled": False } stats = await cache.get_stats() monitor = get_event_monitor() return { "status": "healthy", "enabled": cache.config.enabled, "cache_type": cache.config.cache_type, "memory_size": stats.get('memory', {}).get('size', 0), "sqlite_size": stats.get('sqlite', {}).get('active_entries', 0), "auto_invalidate_running": monitor.running if monitor else False } except Exception as e: return { "status": "error", "error": str(e) }