""" Redis Client Factory Centralized Redis client management with: - Singleton pattern - Connection pooling - Automatic reconnection - Health checks """ import redis import os from typing import Optional from services.exceptions import RedisConnectionError from services.logging_utils import get_service_logger class RedisClientFactory: """ Singleton factory for Redis clients. Benefits: - Centralized configuration - Connection pooling - Lazy initialization - Better error handling """ _instance: Optional[redis.Redis] = None _connection_pool: Optional[redis.ConnectionPool] = None _logger = None @classmethod def _get_logger(cls): """Get logger instance (lazy initialization)""" if cls._logger is None: cls._logger = get_service_logger('redis_factory', None) return cls._logger @classmethod def get_client(cls, strict: bool = False) -> Optional[redis.Redis]: """ Return Redis client (creates if needed). Args: strict: If True, raises exception on connection failures. If False, returns None (for optional Redis usage). Returns: Redis client or None (if strict=False and connection fails) Raises: RedisConnectionError: If strict=True and connection fails """ logger = cls._get_logger() 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: """ Create new Redis client with connection pool. Returns: Configured Redis client Raises: redis.ConnectionError: On connection problems """ logger = cls._get_logger() # 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 to 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 (mainly for tests). Closes existing connections and resets singleton. """ logger = cls._get_logger() 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: """ Check Redis connection. Returns: True if Redis is reachable, False otherwise """ logger = cls._get_logger() 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]: """ Return Redis server info (for monitoring). Returns: Redis info dict or None on error """ logger = cls._get_logger() 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 for Redis client. Args: strict: If True, raises exception on error Returns: Redis client or None """ return RedisClientFactory.get_client(strict=strict) def is_redis_available() -> bool: """ Check if Redis is available. Returns: True if Redis is reachable """ return RedisClientFactory.health_check()