Files
markitect-main/infrastructure/connection_manager.py
tegwick f782ac1f69 fix: Add missing infrastructure files from data access improvements
Add infrastructure components that were created during issue #24
but not properly committed:

- Data access repositories and interfaces
- Connection management infrastructure
- Exception handling framework
- Configuration management
- Documentation from data access pattern improvements

These files are essential infrastructure components that enable
the repository pattern and improved data access strategies.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 08:35:34 +02:00

254 lines
8.0 KiB
Python

"""
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