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>
254 lines
8.0 KiB
Python
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 |