# Redis-based session management system import time import uuid import logging from datetime import datetime from typing import Dict, Any, Optional, List from dataclasses import dataclass, asdict from flask import session, request, g logger = logging.getLogger(__name__) @dataclass class SessionInfo: """Session information stored in Redis""" session_id: str user_id: Optional[str] = None ip_address: Optional[str] = None user_agent: Optional[str] = None created_at: float = None last_activity: float = None request_count: int = 0 resource_count: int = 0 total_bytes_used: int = 0 metadata: Dict[str, Any] = None def __post_init__(self): if self.created_at is None: self.created_at = time.time() if self.last_activity is None: self.last_activity = time.time() if self.metadata is None: self.metadata = {} class RedisSessionManager: """ Session management using Redis for distributed sessions """ def __init__(self, redis_manager, config: Dict[str, Any] = None): self.redis = redis_manager self.config = config or {} # Configuration self.max_session_duration = self.config.get('max_session_duration', 3600) # 1 hour self.max_idle_time = self.config.get('max_idle_time', 900) # 15 minutes self.max_resources_per_session = self.config.get('max_resources_per_session', 100) self.max_bytes_per_session = self.config.get('max_bytes_per_session', 100 * 1024 * 1024) # 100MB logger.info("Redis session manager initialized") def create_session(self, session_id: str = None, user_id: str = None, ip_address: str = None, user_agent: str = None) -> SessionInfo: """Create a new session""" if not session_id: session_id = str(uuid.uuid4()) # Check if session already exists existing = self.get_session(session_id) if existing: logger.warning(f"Session {session_id} already exists") return existing session_info = SessionInfo( session_id=session_id, user_id=user_id, ip_address=ip_address, user_agent=user_agent ) # Save to Redis self._save_session(session_info) # Add to active sessions set self.redis.sadd('active_sessions', session_id) # Update stats self.redis.incr('stats:sessions:created') logger.info(f"Created session {session_id}") return session_info def get_session(self, session_id: str) -> Optional[SessionInfo]: """Get a session by ID""" data = self.redis.get(f'session:{session_id}') if not data: return None # Update last activity session_info = SessionInfo(**data) session_info.last_activity = time.time() self._save_session(session_info) return session_info def update_session_activity(self, session_id: str): """Update session last activity time""" session_info = self.get_session(session_id) if session_info: session_info.last_activity = time.time() session_info.request_count += 1 self._save_session(session_info) def add_resource(self, session_id: str, resource_type: str, resource_id: str = None, path: str = None, size_bytes: int = 0, metadata: Dict[str, Any] = None) -> bool: """Add a resource to a session""" session_info = self.get_session(session_id) if not session_info: logger.warning(f"Session {session_id} not found") return False # Check limits if session_info.resource_count >= self.max_resources_per_session: logger.warning(f"Session {session_id} reached resource limit") # Clean up oldest resources self._cleanup_oldest_resources(session_id, 1) if session_info.total_bytes_used + size_bytes > self.max_bytes_per_session: logger.warning(f"Session {session_id} reached size limit") bytes_to_free = (session_info.total_bytes_used + size_bytes) - self.max_bytes_per_session self._cleanup_resources_by_size(session_id, bytes_to_free) # Generate resource ID if not provided if not resource_id: resource_id = str(uuid.uuid4()) # Store resource info resource_key = f'session:{session_id}:resource:{resource_id}' resource_data = { 'resource_id': resource_id, 'resource_type': resource_type, 'path': path, 'size_bytes': size_bytes, 'created_at': time.time(), 'metadata': metadata or {} } self.redis.set(resource_key, resource_data, expire=self.max_session_duration) # Add to session's resource set self.redis.sadd(f'session:{session_id}:resources', resource_id) # Update session info session_info.resource_count += 1 session_info.total_bytes_used += size_bytes self._save_session(session_info) # Update global stats self.redis.incr('stats:resources:active') self.redis.incr('stats:bytes:active', size_bytes) logger.debug(f"Added {resource_type} resource {resource_id} to session {session_id}") return True def remove_resource(self, session_id: str, resource_id: str) -> bool: """Remove a resource from a session""" # Get resource info resource_key = f'session:{session_id}:resource:{resource_id}' resource_data = self.redis.get(resource_key) if not resource_data: return False # Clean up the actual resource (file, etc.) self._cleanup_resource(resource_data) # Remove from Redis self.redis.delete(resource_key) self.redis.srem(f'session:{session_id}:resources', resource_id) # Update session info session_info = self.get_session(session_id) if session_info: session_info.resource_count -= 1 session_info.total_bytes_used -= resource_data.get('size_bytes', 0) self._save_session(session_info) # Update stats self.redis.decr('stats:resources:active') self.redis.decr('stats:bytes:active', resource_data.get('size_bytes', 0)) self.redis.incr('stats:resources:cleaned') self.redis.incr('stats:bytes:cleaned', resource_data.get('size_bytes', 0)) logger.debug(f"Removed resource {resource_id} from session {session_id}") return True def cleanup_session(self, session_id: str) -> bool: """Clean up a session and all its resources""" session_info = self.get_session(session_id) if not session_info: return False # Get all resources resource_ids = self.redis.smembers(f'session:{session_id}:resources') # Clean up each resource for resource_id in resource_ids: self.remove_resource(session_id, resource_id) # Remove session data self.redis.delete(f'session:{session_id}') self.redis.delete(f'session:{session_id}:resources') self.redis.srem('active_sessions', session_id) # Update stats self.redis.incr('stats:sessions:cleaned') logger.info(f"Cleaned up session {session_id}") return True def cleanup_expired_sessions(self): """Clean up sessions that have exceeded max duration""" now = time.time() active_sessions = self.redis.smembers('active_sessions') for session_id in active_sessions: session_info = self.get_session(session_id) if session_info and (now - session_info.created_at > self.max_session_duration): logger.info(f"Cleaning up expired session {session_id}") self.cleanup_session(session_id) def cleanup_idle_sessions(self): """Clean up sessions that have been idle too long""" now = time.time() active_sessions = self.redis.smembers('active_sessions') for session_id in active_sessions: session_info = self.get_session(session_id) if session_info and (now - session_info.last_activity > self.max_idle_time): logger.info(f"Cleaning up idle session {session_id}") self.cleanup_session(session_id) def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]: """Get detailed information about a session""" session_info = self.get_session(session_id) if not session_info: return None # Get resources resource_ids = self.redis.smembers(f'session:{session_id}:resources') resources = [] for resource_id in resource_ids: resource_data = self.redis.get(f'session:{session_id}:resource:{resource_id}') if resource_data: resources.append({ 'resource_id': resource_data['resource_id'], 'resource_type': resource_data['resource_type'], 'size_bytes': resource_data['size_bytes'], 'created_at': datetime.fromtimestamp(resource_data['created_at']).isoformat() }) return { 'session_id': session_info.session_id, 'user_id': session_info.user_id, 'ip_address': session_info.ip_address, 'created_at': datetime.fromtimestamp(session_info.created_at).isoformat(), 'last_activity': datetime.fromtimestamp(session_info.last_activity).isoformat(), 'duration_seconds': int(time.time() - session_info.created_at), 'idle_seconds': int(time.time() - session_info.last_activity), 'request_count': session_info.request_count, 'resource_count': session_info.resource_count, 'total_bytes_used': session_info.total_bytes_used, 'resources': resources } def get_all_sessions_info(self) -> List[Dict[str, Any]]: """Get information about all active sessions""" active_sessions = self.redis.smembers('active_sessions') return [ self.get_session_info(session_id) for session_id in active_sessions if self.get_session_info(session_id) ] def get_stats(self) -> Dict[str, Any]: """Get session manager statistics""" active_sessions = self.redis.scard('active_sessions') return { 'active_sessions': active_sessions, 'total_sessions_created': self.redis.get('stats:sessions:created', 0), 'total_sessions_cleaned': self.redis.get('stats:sessions:cleaned', 0), 'active_resources': self.redis.get('stats:resources:active', 0), 'total_resources_cleaned': self.redis.get('stats:resources:cleaned', 0), 'active_bytes': self.redis.get('stats:bytes:active', 0), 'total_bytes_cleaned': self.redis.get('stats:bytes:cleaned', 0) } def _save_session(self, session_info: SessionInfo): """Save session info to Redis""" key = f'session:{session_info.session_id}' data = asdict(session_info) self.redis.set(key, data, expire=self.max_session_duration) def _cleanup_resource(self, resource_data: Dict[str, Any]): """Clean up a resource (e.g., delete file)""" import os if resource_data.get('resource_type') in ['audio_file', 'temp_file']: path = resource_data.get('path') if path and os.path.exists(path): try: os.remove(path) logger.debug(f"Removed file {path}") except Exception as e: logger.error(f"Failed to remove file {path}: {e}") def _cleanup_oldest_resources(self, session_id: str, count: int): """Clean up oldest resources from a session""" resource_ids = list(self.redis.smembers(f'session:{session_id}:resources')) # Get resource creation times resources_with_time = [] for resource_id in resource_ids: resource_data = self.redis.get(f'session:{session_id}:resource:{resource_id}') if resource_data: resources_with_time.append((resource_id, resource_data.get('created_at', 0))) # Sort by creation time and remove oldest resources_with_time.sort(key=lambda x: x[1]) for resource_id, _ in resources_with_time[:count]: self.remove_resource(session_id, resource_id) def _cleanup_resources_by_size(self, session_id: str, bytes_to_free: int): """Clean up resources to free up space""" resource_ids = list(self.redis.smembers(f'session:{session_id}:resources')) # Get resource sizes resources_with_size = [] for resource_id in resource_ids: resource_data = self.redis.get(f'session:{session_id}:resource:{resource_id}') if resource_data: resources_with_size.append((resource_id, resource_data.get('size_bytes', 0))) # Sort by size (largest first) and remove until we've freed enough resources_with_size.sort(key=lambda x: x[1], reverse=True) freed_bytes = 0 for resource_id, size in resources_with_size: if freed_bytes >= bytes_to_free: break freed_bytes += size self.remove_resource(session_id, resource_id) def init_app(app): """Initialize Redis session management for Flask app""" # Get Redis manager redis_manager = getattr(app, 'redis', None) if not redis_manager: raise RuntimeError("Redis manager not initialized. Call init_redis() first.") config = { 'max_session_duration': app.config.get('MAX_SESSION_DURATION', 3600), 'max_idle_time': app.config.get('MAX_SESSION_IDLE_TIME', 900), 'max_resources_per_session': app.config.get('MAX_RESOURCES_PER_SESSION', 100), 'max_bytes_per_session': app.config.get('MAX_BYTES_PER_SESSION', 100 * 1024 * 1024) } manager = RedisSessionManager(redis_manager, config) app.redis_session_manager = manager # Add before_request handler @app.before_request def before_request_session(): # Get or create session session_id = session.get('session_id') if not session_id: session_id = str(uuid.uuid4()) session['session_id'] = session_id session.permanent = True # Get session from manager user_session = manager.get_session(session_id) if not user_session: user_session = manager.create_session( session_id=session_id, ip_address=request.remote_addr, user_agent=request.headers.get('User-Agent') ) # Update activity manager.update_session_activity(session_id) # Store in g for request access g.user_session = user_session g.session_manager = manager logger.info("Redis session management initialized")