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>
This commit is contained in:
254
infrastructure/connection_manager.py
Normal file
254
infrastructure/connection_manager.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user