- Added `config.py` for centralized configuration management including Sync, API, Advoware, EspoCRM, Redis, Logging, Calendar Sync, and Feature Flags. - Created `exceptions.py` with a hierarchy of custom exceptions for integration errors, API errors, sync errors, and Redis errors. - Developed `logging_utils.py` for a unified logging wrapper supporting structured logging and performance tracking. - Defined Pydantic models in `models.py` for data validation of Advoware and EspoCRM entities, including sync operation models. - Introduced `redis_client.py` for a centralized Redis client factory with connection pooling, automatic reconnection, and health checks.
192 lines
5.5 KiB
Python
192 lines
5.5 KiB
Python
"""
|
|
Redis Client Factory
|
|
|
|
Zentralisierte Redis-Client-Verwaltung mit:
|
|
- Singleton Pattern
|
|
- Connection Pooling
|
|
- Automatic Reconnection
|
|
- Health Checks
|
|
"""
|
|
|
|
import redis
|
|
import os
|
|
import logging
|
|
from typing import Optional
|
|
from services.exceptions import RedisConnectionError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RedisClientFactory:
|
|
"""
|
|
Singleton Factory für Redis Clients.
|
|
|
|
Vorteile:
|
|
- Eine zentrale Konfiguration
|
|
- Connection Pooling
|
|
- Lazy Initialization
|
|
- Besseres Error Handling
|
|
"""
|
|
|
|
_instance: Optional[redis.Redis] = None
|
|
_connection_pool: Optional[redis.ConnectionPool] = None
|
|
|
|
@classmethod
|
|
def get_client(cls, strict: bool = False) -> Optional[redis.Redis]:
|
|
"""
|
|
Gibt Redis Client zurück (erstellt wenn nötig).
|
|
|
|
Args:
|
|
strict: Wenn True, wirft Exception bei Verbindungsfehlern.
|
|
Wenn False, gibt None zurück (für optionale Redis-Nutzung).
|
|
|
|
Returns:
|
|
Redis client oder None (wenn strict=False und Verbindung fehlschlägt)
|
|
|
|
Raises:
|
|
RedisConnectionError: Wenn strict=True und Verbindung fehlschlägt
|
|
"""
|
|
if cls._instance is None:
|
|
try:
|
|
cls._instance = cls._create_client()
|
|
logger.info("Redis client created successfully")
|
|
except Exception as e:
|
|
logger.error(f"Failed to create Redis client: {e}")
|
|
if strict:
|
|
raise RedisConnectionError(
|
|
f"Could not connect to Redis: {e}",
|
|
operation="get_client"
|
|
)
|
|
logger.warning("Redis unavailable - continuing without caching")
|
|
return None
|
|
|
|
return cls._instance
|
|
|
|
@classmethod
|
|
def _create_client(cls) -> redis.Redis:
|
|
"""
|
|
Erstellt neuen Redis Client mit Connection Pool.
|
|
|
|
Returns:
|
|
Configured Redis client
|
|
|
|
Raises:
|
|
redis.ConnectionError: Bei Verbindungsproblemen
|
|
"""
|
|
# Load configuration from environment
|
|
redis_host = os.getenv('REDIS_HOST', 'localhost')
|
|
redis_port = int(os.getenv('REDIS_PORT', '6379'))
|
|
redis_db = int(os.getenv('REDIS_DB_ADVOWARE_CACHE', '1'))
|
|
redis_timeout = int(os.getenv('REDIS_TIMEOUT_SECONDS', '5'))
|
|
redis_max_connections = int(os.getenv('REDIS_MAX_CONNECTIONS', '50'))
|
|
|
|
logger.info(
|
|
f"Creating Redis client: {redis_host}:{redis_port} "
|
|
f"(db={redis_db}, timeout={redis_timeout}s)"
|
|
)
|
|
|
|
# Create connection pool
|
|
if cls._connection_pool is None:
|
|
cls._connection_pool = redis.ConnectionPool(
|
|
host=redis_host,
|
|
port=redis_port,
|
|
db=redis_db,
|
|
socket_timeout=redis_timeout,
|
|
socket_connect_timeout=redis_timeout,
|
|
max_connections=redis_max_connections,
|
|
decode_responses=True # Auto-decode bytes zu strings
|
|
)
|
|
|
|
# Create client from pool
|
|
client = redis.Redis(connection_pool=cls._connection_pool)
|
|
|
|
# Verify connection
|
|
client.ping()
|
|
|
|
return client
|
|
|
|
@classmethod
|
|
def reset(cls) -> None:
|
|
"""
|
|
Reset factory state (hauptsächlich für Tests).
|
|
|
|
Schließt bestehende Verbindungen und setzt Singleton zurück.
|
|
"""
|
|
if cls._instance:
|
|
try:
|
|
cls._instance.close()
|
|
except Exception as e:
|
|
logger.warning(f"Error closing Redis client: {e}")
|
|
|
|
if cls._connection_pool:
|
|
try:
|
|
cls._connection_pool.disconnect()
|
|
except Exception as e:
|
|
logger.warning(f"Error closing connection pool: {e}")
|
|
|
|
cls._instance = None
|
|
cls._connection_pool = None
|
|
logger.info("Redis factory reset")
|
|
|
|
@classmethod
|
|
def health_check(cls) -> bool:
|
|
"""
|
|
Prüft Redis-Verbindung.
|
|
|
|
Returns:
|
|
True wenn Redis erreichbar, False sonst
|
|
"""
|
|
try:
|
|
client = cls.get_client(strict=False)
|
|
if client is None:
|
|
return False
|
|
|
|
client.ping()
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"Redis health check failed: {e}")
|
|
return False
|
|
|
|
@classmethod
|
|
def get_info(cls) -> Optional[dict]:
|
|
"""
|
|
Gibt Redis Server Info zurück (für Monitoring).
|
|
|
|
Returns:
|
|
Redis info dict oder None bei Fehler
|
|
"""
|
|
try:
|
|
client = cls.get_client(strict=False)
|
|
if client is None:
|
|
return None
|
|
|
|
return client.info()
|
|
except Exception as e:
|
|
logger.error(f"Failed to get Redis info: {e}")
|
|
return None
|
|
|
|
|
|
# ========== Convenience Functions ==========
|
|
|
|
def get_redis_client(strict: bool = False) -> Optional[redis.Redis]:
|
|
"""
|
|
Convenience function für Redis Client.
|
|
|
|
Args:
|
|
strict: Wenn True, wirft Exception bei Fehler
|
|
|
|
Returns:
|
|
Redis client oder None
|
|
"""
|
|
return RedisClientFactory.get_client(strict=strict)
|
|
|
|
|
|
def is_redis_available() -> bool:
|
|
"""
|
|
Prüft ob Redis verfügbar ist.
|
|
|
|
Returns:
|
|
True wenn Redis erreichbar
|
|
"""
|
|
return RedisClientFactory.health_check()
|