- Implement AdvowareHistoryService for fetching and creating history entries. - Implement AdvowareWatcherService for file operations including listing, downloading, and uploading with Blake3 hash verification. - Introduce Blake3 utility functions for hash computation and verification. - Create document sync cron step to poll Redis for pending Aktennummern and emit sync events. - Develop document sync event handler to manage 3-way merge synchronization for Akten, including metadata updates and error handling.
211 lines
6.2 KiB
Python
211 lines
6.2 KiB
Python
"""
|
|
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_password = os.getenv('REDIS_PASSWORD', None) # Optional password
|
|
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:
|
|
pool_kwargs = {
|
|
'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
|
|
}
|
|
|
|
# Add password if configured
|
|
if redis_password:
|
|
pool_kwargs['password'] = redis_password
|
|
logger.info("Redis authentication enabled")
|
|
|
|
cls._connection_pool = redis.ConnectionPool(**pool_kwargs)
|
|
|
|
# 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()
|