Implement central configuration, custom exceptions, logging utilities, Pydantic models, and Redis client for BitByLaw integration
- 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.
This commit is contained in:
191
services/redis_client.py
Normal file
191
services/redis_client.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user