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