python-detector-worker/core/storage/redis.py
2025-09-24 20:29:31 +07:00

478 lines
No EOL
16 KiB
Python

"""
Redis Operations Module.
Handles Redis connections, image storage, and pub/sub messaging.
"""
import logging
import json
import time
from typing import Optional, Dict, Any, Union
import asyncio
import cv2
import numpy as np
import redis.asyncio as redis
from redis.exceptions import ConnectionError, TimeoutError
logger = logging.getLogger(__name__)
class RedisManager:
"""
Manages Redis connections and operations for the detection pipeline.
Handles image storage with region cropping and pub/sub messaging.
"""
def __init__(self, redis_config: Dict[str, Any]):
"""
Initialize Redis manager with configuration.
Args:
redis_config: Redis configuration dictionary
"""
self.config = redis_config
self.redis_client: Optional[redis.Redis] = None
# Connection parameters
self.host = redis_config.get('host', 'localhost')
self.port = redis_config.get('port', 6379)
self.password = redis_config.get('password')
self.db = redis_config.get('db', 0)
self.decode_responses = redis_config.get('decode_responses', True)
# Connection pool settings
self.max_connections = redis_config.get('max_connections', 10)
self.socket_timeout = redis_config.get('socket_timeout', 5)
self.socket_connect_timeout = redis_config.get('socket_connect_timeout', 5)
self.health_check_interval = redis_config.get('health_check_interval', 30)
# Statistics
self.stats = {
'images_stored': 0,
'messages_published': 0,
'connection_errors': 0,
'operations_successful': 0,
'operations_failed': 0
}
logger.info(f"RedisManager initialized for {self.host}:{self.port}")
async def initialize(self) -> bool:
"""
Initialize Redis connection and test connectivity.
Returns:
True if successful, False otherwise
"""
try:
# Validate configuration
if not self._validate_config():
return False
# Create Redis connection
self.redis_client = redis.Redis(
host=self.host,
port=self.port,
password=self.password,
db=self.db,
decode_responses=self.decode_responses,
max_connections=self.max_connections,
socket_timeout=self.socket_timeout,
socket_connect_timeout=self.socket_connect_timeout,
health_check_interval=self.health_check_interval
)
# Test connection
await self.redis_client.ping()
logger.info(f"Successfully connected to Redis at {self.host}:{self.port}")
return True
except ConnectionError as e:
logger.error(f"Failed to connect to Redis: {e}")
self.stats['connection_errors'] += 1
return False
except Exception as e:
logger.error(f"Error initializing Redis connection: {e}", exc_info=True)
self.stats['connection_errors'] += 1
return False
def _validate_config(self) -> bool:
"""
Validate Redis configuration parameters.
Returns:
True if valid, False otherwise
"""
required_fields = ['host', 'port']
for field in required_fields:
if field not in self.config:
logger.error(f"Missing required Redis config field: {field}")
return False
if not isinstance(self.port, int) or self.port <= 0:
logger.error(f"Invalid Redis port: {self.port}")
return False
return True
async def is_connected(self) -> bool:
"""
Check if Redis connection is active.
Returns:
True if connected, False otherwise
"""
try:
if self.redis_client:
await self.redis_client.ping()
return True
except Exception:
pass
return False
async def save_image(self,
key: str,
image: np.ndarray,
expire_seconds: Optional[int] = None,
image_format: str = 'jpeg',
quality: int = 90) -> bool:
"""
Save image to Redis with optional expiration.
Args:
key: Redis key for the image
image: Image array to save
expire_seconds: Optional expiration time in seconds
image_format: Image format ('jpeg' or 'png')
quality: JPEG quality (1-100)
Returns:
True if successful, False otherwise
"""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
self.stats['operations_failed'] += 1
return False
# Encode image
encoded_image = self._encode_image(image, image_format, quality)
if encoded_image is None:
logger.error("Failed to encode image")
self.stats['operations_failed'] += 1
return False
# Save to Redis
if expire_seconds:
await self.redis_client.setex(key, expire_seconds, encoded_image)
logger.debug(f"Saved image to Redis with key: {key} (expires in {expire_seconds}s)")
else:
await self.redis_client.set(key, encoded_image)
logger.debug(f"Saved image to Redis with key: {key}")
self.stats['images_stored'] += 1
self.stats['operations_successful'] += 1
return True
except Exception as e:
logger.error(f"Error saving image to Redis: {e}", exc_info=True)
self.stats['operations_failed'] += 1
return False
async def get_image(self, key: str) -> Optional[np.ndarray]:
"""
Retrieve image from Redis.
Args:
key: Redis key for the image
Returns:
Image array or None if not found
"""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
self.stats['operations_failed'] += 1
return None
# Get image data from Redis
image_data = await self.redis_client.get(key)
if image_data is None:
logger.debug(f"Image not found for key: {key}")
return None
# Decode image
image_array = np.frombuffer(image_data, np.uint8)
image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
if image is not None:
logger.debug(f"Retrieved image from Redis with key: {key}")
self.stats['operations_successful'] += 1
return image
else:
logger.error(f"Failed to decode image for key: {key}")
self.stats['operations_failed'] += 1
return None
except Exception as e:
logger.error(f"Error retrieving image from Redis: {e}", exc_info=True)
self.stats['operations_failed'] += 1
return None
async def delete_image(self, key: str) -> bool:
"""
Delete image from Redis.
Args:
key: Redis key for the image
Returns:
True if successful, False otherwise
"""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
self.stats['operations_failed'] += 1
return False
result = await self.redis_client.delete(key)
if result > 0:
logger.debug(f"Deleted image from Redis with key: {key}")
self.stats['operations_successful'] += 1
return True
else:
logger.debug(f"Image not found for deletion: {key}")
return False
except Exception as e:
logger.error(f"Error deleting image from Redis: {e}", exc_info=True)
self.stats['operations_failed'] += 1
return False
async def publish_message(self, channel: str, message: Union[str, Dict]) -> int:
"""
Publish message to Redis channel.
Args:
channel: Redis channel name
message: Message to publish (string or dict)
Returns:
Number of subscribers that received the message, -1 on error
"""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
self.stats['operations_failed'] += 1
return -1
# Convert dict to JSON string if needed
if isinstance(message, dict):
message_str = json.dumps(message)
else:
message_str = str(message)
# Test connection before publishing
await self.redis_client.ping()
# Publish message
result = await self.redis_client.publish(channel, message_str)
logger.info(f"Published message to Redis channel '{channel}': {message_str}")
logger.info(f"Redis publish result (subscribers count): {result}")
if result == 0:
logger.warning(f"No subscribers listening to channel '{channel}'")
else:
logger.info(f"Message delivered to {result} subscriber(s)")
self.stats['messages_published'] += 1
self.stats['operations_successful'] += 1
return result
except Exception as e:
logger.error(f"Error publishing message to Redis: {e}", exc_info=True)
self.stats['operations_failed'] += 1
return -1
async def subscribe_to_channel(self, channel: str, callback=None):
"""
Subscribe to Redis channel (for future use).
Args:
channel: Redis channel name
callback: Optional callback function for messages
"""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
return
pubsub = self.redis_client.pubsub()
await pubsub.subscribe(channel)
logger.info(f"Subscribed to Redis channel: {channel}")
if callback:
async for message in pubsub.listen():
if message['type'] == 'message':
try:
await callback(message['data'])
except Exception as e:
logger.error(f"Error in message callback: {e}")
except Exception as e:
logger.error(f"Error subscribing to Redis channel: {e}", exc_info=True)
async def set_key(self, key: str, value: Union[str, bytes], expire_seconds: Optional[int] = None) -> bool:
"""
Set a key-value pair in Redis.
Args:
key: Redis key
value: Value to store
expire_seconds: Optional expiration time in seconds
Returns:
True if successful, False otherwise
"""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
self.stats['operations_failed'] += 1
return False
if expire_seconds:
await self.redis_client.setex(key, expire_seconds, value)
else:
await self.redis_client.set(key, value)
logger.debug(f"Set Redis key: {key}")
self.stats['operations_successful'] += 1
return True
except Exception as e:
logger.error(f"Error setting Redis key: {e}", exc_info=True)
self.stats['operations_failed'] += 1
return False
async def get_key(self, key: str) -> Optional[Union[str, bytes]]:
"""
Get value for a Redis key.
Args:
key: Redis key
Returns:
Value or None if not found
"""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
self.stats['operations_failed'] += 1
return None
value = await self.redis_client.get(key)
if value is not None:
logger.debug(f"Retrieved Redis key: {key}")
self.stats['operations_successful'] += 1
return value
except Exception as e:
logger.error(f"Error getting Redis key: {e}", exc_info=True)
self.stats['operations_failed'] += 1
return None
async def delete_key(self, key: str) -> bool:
"""
Delete a Redis key.
Args:
key: Redis key
Returns:
True if successful, False otherwise
"""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
self.stats['operations_failed'] += 1
return False
result = await self.redis_client.delete(key)
if result > 0:
logger.debug(f"Deleted Redis key: {key}")
self.stats['operations_successful'] += 1
return True
else:
logger.debug(f"Redis key not found: {key}")
return False
except Exception as e:
logger.error(f"Error deleting Redis key: {e}", exc_info=True)
self.stats['operations_failed'] += 1
return False
def _encode_image(self, image: np.ndarray, image_format: str, quality: int) -> Optional[bytes]:
"""
Encode image to bytes for Redis storage.
Args:
image: Image array
image_format: Image format ('jpeg' or 'png')
quality: JPEG quality (1-100)
Returns:
Encoded image bytes or None on error
"""
try:
format_lower = image_format.lower()
if format_lower == 'jpeg' or format_lower == 'jpg':
encode_params = [cv2.IMWRITE_JPEG_QUALITY, quality]
success, buffer = cv2.imencode('.jpg', image, encode_params)
elif format_lower == 'png':
success, buffer = cv2.imencode('.png', image)
else:
logger.warning(f"Unknown image format '{image_format}', using JPEG")
encode_params = [cv2.IMWRITE_JPEG_QUALITY, quality]
success, buffer = cv2.imencode('.jpg', image, encode_params)
if success:
return buffer.tobytes()
else:
logger.error(f"Failed to encode image as {image_format}")
return None
except Exception as e:
logger.error(f"Error encoding image: {e}", exc_info=True)
return None
def get_statistics(self) -> Dict[str, Any]:
"""
Get Redis manager statistics.
Returns:
Dictionary with statistics
"""
return {
**self.stats,
'connected': self.redis_client is not None,
'host': self.host,
'port': self.port,
'db': self.db
}
def cleanup(self):
"""Cleanup Redis connection."""
if self.redis_client:
# Note: redis.asyncio doesn't have a synchronous close method
# The connection will be closed when the event loop shuts down
self.redis_client = None
logger.info("Redis connection cleaned up")
async def aclose(self):
"""Async cleanup for Redis connection."""
if self.redis_client:
await self.redis_client.aclose()
self.redis_client = None
logger.info("Redis connection closed")