""" 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 import os import time from datetime import datetime, timedelta sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) from auth.dependencies import get_current_user from auth.models import CurrentUser from ..cache import get_cache, get_event_monitor, toggle_event_monitor router = APIRouter(prefix="/cache", 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) }