""" Connection management infrastructure for MarkiTect. Provides HTTP session pooling, database connection management, and resource lifecycle management with proper cleanup. """ import asyncio import sqlite3 from typing import Optional, Dict, Any from contextlib import asynccontextmanager from dataclasses import dataclass import aiohttp from infrastructure.logging import get_logger logger = get_logger(__name__) @dataclass class DataSourceConfig: """Configuration for data source connections.""" # HTTP Configuration gitea_base_url: str gitea_token: str connection_pool_size: int = 20 connection_per_host: int = 5 request_timeout: int = 30 keepalive_timeout: int = 60 # Database Configuration database_path: str = "markitect.db" database_pool_size: int = 10 database_timeout: int = 30 # Retry Configuration max_retries: int = 3 retry_backoff_factor: float = 1.5 retry_base_delay: float = 1.0 class ConnectionManager: """ Manages connection pooling for HTTP and database operations. Provides centralized resource management with proper lifecycle handling, connection pooling, and automatic cleanup. """ def __init__(self, config: DataSourceConfig): self.config = config self._http_session: Optional[aiohttp.ClientSession] = None self._db_pool: Optional[sqlite3.Connection] = None self._lock = asyncio.Lock() async def get_http_session(self) -> aiohttp.ClientSession: """ Get HTTP session with connection pooling. Returns: Configured aiohttp.ClientSession with connection pooling, timeout settings, and authentication headers. """ if self._http_session is None or self._http_session.closed: async with self._lock: if self._http_session is None or self._http_session.closed: await self._create_http_session() return self._http_session async def _create_http_session(self): """Create new HTTP session with optimized settings.""" connector = aiohttp.TCPConnector( limit=self.config.connection_pool_size, limit_per_host=self.config.connection_per_host, keepalive_timeout=self.config.keepalive_timeout, enable_cleanup_closed=True ) timeout = aiohttp.ClientTimeout(total=self.config.request_timeout) headers = {} if self.config.gitea_token: headers['Authorization'] = f'token {self.config.gitea_token}' self._http_session = aiohttp.ClientSession( base_url=self.config.gitea_base_url, connector=connector, timeout=timeout, headers=headers ) logger.info(f"Created HTTP session with pool size {self.config.connection_pool_size}") def get_database_connection(self) -> sqlite3.Connection: """ Get database connection with optimized settings. Returns: Configured SQLite connection with proper timeout and performance settings. """ if self._db_pool is None: self._create_database_connection() return self._db_pool def _create_database_connection(self): """Create database connection with optimized settings.""" self._db_pool = sqlite3.connect( self.config.database_path, timeout=self.config.database_timeout, check_same_thread=False ) # Optimize SQLite settings for performance self._db_pool.execute("PRAGMA journal_mode=WAL") self._db_pool.execute("PRAGMA synchronous=NORMAL") self._db_pool.execute("PRAGMA cache_size=10000") self._db_pool.execute("PRAGMA temp_store=MEMORY") logger.info(f"Created database connection to {self.config.database_path}") @asynccontextmanager async def transaction(self): """ Context manager for database transactions. Automatically handles commit/rollback and ensures proper resource cleanup. """ conn = self.get_database_connection() conn.execute("BEGIN") try: yield conn conn.commit() logger.debug("Transaction committed successfully") except Exception as e: conn.rollback() logger.error(f"Transaction rolled back due to error: {e}") raise async def close(self): """Clean up all connections and resources.""" if self._http_session and not self._http_session.closed: await self._http_session.close() logger.info("HTTP session closed") if self._db_pool: self._db_pool.close() logger.info("Database connection closed") async def health_check(self) -> Dict[str, Any]: """ Perform health check on all connections. Returns: Dictionary with status of HTTP and database connections. """ health_status = { "http_session": "unknown", "database": "unknown", "timestamp": asyncio.get_event_loop().time() } # Check HTTP session try: if self._http_session and not self._http_session.closed: # Simple ping to check connectivity async with self._http_session.get("/api/v1/version") as response: if response.status < 400: health_status["http_session"] = "healthy" else: health_status["http_session"] = "degraded" else: health_status["http_session"] = "disconnected" except Exception as e: health_status["http_session"] = f"error: {str(e)}" logger.warning(f"HTTP health check failed: {e}") # Check database connection try: if self._db_pool: self._db_pool.execute("SELECT 1").fetchone() health_status["database"] = "healthy" else: health_status["database"] = "disconnected" except Exception as e: health_status["database"] = f"error: {str(e)}" logger.warning(f"Database health check failed: {e}") return health_status class RetryConfig: """Configuration for retry mechanisms.""" def __init__( self, max_attempts: int = 3, base_delay: float = 1.0, backoff_factor: float = 2.0, max_delay: float = 60.0 ): self.max_attempts = max_attempts self.base_delay = base_delay self.backoff_factor = backoff_factor self.max_delay = max_delay def retry_with_backoff(retry_config: RetryConfig): """ Decorator for implementing retry with exponential backoff. Args: retry_config: Configuration for retry behavior Returns: Decorator function that wraps methods with retry logic """ def decorator(func): async def wrapper(*args, **kwargs): last_exception = None for attempt in range(retry_config.max_attempts): try: return await func(*args, **kwargs) except Exception as e: last_exception = e if attempt == retry_config.max_attempts - 1: # Last attempt, don't wait break # Calculate delay with exponential backoff delay = min( retry_config.base_delay * (retry_config.backoff_factor ** attempt), retry_config.max_delay ) logger.warning( f"Attempt {attempt + 1}/{retry_config.max_attempts} failed for {func.__name__}: {e}. " f"Retrying in {delay:.1f}s" ) await asyncio.sleep(delay) # All attempts failed logger.error(f"All {retry_config.max_attempts} attempts failed for {func.__name__}") raise last_exception return wrapper return decorator