Files
motia-iii/services/redis_client.py
bsiggel 69a48f5f9a 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.
2026-03-03 17:18:49 +00:00

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()