""" 配置同步模块 实现MySQL数据库连接管理、Redis Pub/Sub订阅、配置缓存与动态刷新 """ import json import logging import threading import time from datetime import datetime from typing import Any, Callable, Dict, List, Optional, Set import redis from redis import Redis from redis.client import PubSub from config.settings import get_settings, RedisConfig from config.database import get_database_manager, DatabaseManager from config.config_models import CameraInfo as CameraInfoModel, ROIInfo, ConfigVersion from utils.version_control import get_version_control logger = logging.getLogger(__name__) class ConfigCache: """配置缓存管理类""" def __init__(self, max_size: int = 1000, ttl: int = 300): self._cache: Dict[str, Any] = {} self._access_times: Dict[str, float] = {} self._max_size = max_size self._ttl = ttl self._lock = threading.RLock() def get(self, key: str) -> Optional[Any]: """从缓存获取配置""" with self._lock: if key in self._cache: access_time = self._access_times.get(key, 0) if (time.time() - access_time) < self._ttl: self._access_times[key] = time.time() return self._cache[key] else: self._delete(key) return None def set(self, key: str, value: Any): """设置配置到缓存""" with self._lock: if len(self._cache) >= self._max_size: self._evict_lru() self._cache[key] = value self._access_times[key] = time.time() def delete(self, key: str): """删除缓存项""" with self._lock: self._delete(key) def _delete(self, key: str): """内部删除方法(不获取锁)""" self._cache.pop(key, None) self._access_times.pop(key, None) def _evict_lru(self): """淘汰最少使用的缓存项""" if not self._access_times: return min_access_time = min(self._access_times.values()) lru_keys = [k for k, v in self._access_times.items() if v == min_access_time] for key in lru_keys[:10]: self._delete(key) def clear(self): """清空缓存""" with self._lock: self._cache.clear() self._access_times.clear() def get_stats(self) -> Dict[str, Any]: """获取缓存统计信息""" with self._lock: return { "size": len(self._cache), "max_size": self._max_size, "ttl": self._ttl, } class ConfigSyncManager: """配置同步管理器类""" _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): if self._initialized: return settings = get_settings() self._config_version = settings.config_version self._cache = ConfigCache() self._redis_client = None self._redis_pubsub = None self._pubsub_thread = None self._stop_event = threading.Event() self._callbacks: Dict[str, Set[Callable]] = {} self._db_manager = None self._initialized = True self._init_redis() self._version_control = get_version_control() def _init_redis(self): """初始化Redis连接""" try: settings = get_settings() redis_config = settings.redis self._redis_client = redis.Redis( host=redis_config.host, port=redis_config.port, db=redis_config.db, password=redis_config.password, decode_responses=redis_config.decode_responses, socket_connect_timeout=10, socket_timeout=10, retry_on_timeout=True, ) self._redis_client.ping() logger.info(f"Redis连接成功: {redis_config.host}:{redis_config.port}") except Exception as e: logger.error(f"Redis连接失败: {e}") self._redis_client = None def _init_database(self): """初始化数据库连接""" if self._db_manager is None: self._db_manager = get_database_manager() @property def config_version(self) -> str: """获取当前配置版本""" return self._config_version def register_callback(self, topic: str, callback: Callable): """注册配置变更回调函数""" if topic not in self._callbacks: self._callbacks[topic] = set() self._callbacks[topic].add(callback) logger.info(f"已注册配置变更回调: {topic}") def unregister_callback(self, topic: str, callback: Callable): """注销配置变更回调函数""" if topic in self._callbacks: self._callbacks[topic].discard(callback) def _notify_callbacks(self, topic: str, data: Dict[str, Any]): """触发配置变更回调""" if topic in self._callbacks: for callback in self._callbacks[topic]: try: callback(topic, data) except Exception as e: logger.error(f"配置变更回调执行失败: {e}") def _subscribe_config_updates(self): """订阅配置更新主题""" if not self._redis_client: logger.warning("Redis未连接,无法订阅配置更新") return try: self._redis_pubsub = self._redis_client.pubsub() self._redis_pubsub.subscribe("config_update") logger.info("已订阅config_update主题") for message in self._redis_pubsub.listen(): if self._stop_event.is_set(): break if message["type"] == "message": try: data = json.loads(message["data"]) self._handle_config_update(data) except Exception as e: logger.error(f"处理配置更新消息失败: {e}") except Exception as e: logger.error(f"配置更新订阅异常: {e}") def _handle_config_update(self, data: Dict[str, Any]): """处理配置更新消息""" update_type = data.get("type", "full") affected_items = data.get("affected_items", []) version = data.get("version", self._config_version) logger.info(f"收到配置更新通知: type={update_type}, items={affected_items}") if "camera" in affected_items or "all" in affected_items: self._cache.delete("cameras") if "roi" in affected_items or "all" in affected_items: self._cache.delete("rois") self._config_version = version self._notify_callbacks("config_update", data) self._version_control.record_update( version=version, update_type="配置更新", description=f"云端配置更新,影响范围: {', '.join(affected_items)}", updated_by="云端系统", affected_items=affected_items, details=data ) def start_config_subscription(self): """启动配置订阅线程""" if self._pubsub_thread is None or not self._pubsub_thread.is_alive(): self._stop_event.clear() self._pubsub_thread = threading.Thread( target=self._subscribe_config_updates, name="ConfigSubscription", daemon=True ) self._pubsub_thread.start() logger.info("配置订阅线程已启动") def stop_config_subscription(self): """停止配置订阅线程""" self._stop_event.set() if self._pubsub_thread and self._pubsub_thread.is_alive(): self._pubsub_thread.join(timeout=5) logger.info("配置订阅线程已停止") def get_cameras(self, force_refresh: bool = False) -> List[CameraInfoModel]: """获取摄像头配置列表""" cache_key = "cameras" if not force_refresh: cached = self._cache.get(cache_key) if cached is not None: return cached self._init_database() if self._db_manager is None: logger.warning("数据库管理器不可用,返回空摄像头列表") return [] try: cameras = self._db_manager.get_camera_info() result = [CameraInfoModel.from_dict(c) for c in cameras] self._cache.set(cache_key, result) logger.info(f"已加载摄像头配置: {len(result)} 个") return result except Exception as e: logger.error(f"获取摄像头配置失败: {e}") cached = self._cache.get(cache_key) return cached or [] def get_roi_configs(self, camera_id: Optional[str] = None, force_refresh: bool = False) -> List[ROIInfo]: """获取ROI配置列表""" cache_key = f"rois_{camera_id}" if camera_id else "rois_all" if not force_refresh: cached = self._cache.get(cache_key) if cached is not None: return cached self._init_database() if self._db_manager is None: logger.warning("数据库管理器不可用,返回空ROI配置列表") return [] try: roi_configs = self._db_manager.get_roi_configs(camera_id=camera_id) result = [ROIInfo.from_dict(r) for r in roi_configs] self._cache.set(cache_key, result) logger.info(f"已加载ROI配置: {len(result)} 个") return result except Exception as e: logger.error(f"获取ROI配置失败: {e}") cached = self._cache.get(cache_key) return cached or [] def get_camera_rois(self, camera_id: str) -> List[ROIInfo]: """获取指定摄像头的ROI配置""" return self.get_roi_configs(camera_id=camera_id) def get_config_by_id(self, config_type: str, config_id: str) -> Optional[Dict[str, Any]]: """根据ID获取配置""" self._init_database() try: if config_type == "camera": cameras = self._db_manager.get_camera_info(camera_id) return cameras[0] if cameras else None elif config_type == "roi": rois = self._db_manager.get_roi_configs(roi_id=config_id) return rois[0] if rois else None except Exception as e: logger.error(f"获取配置失败: {e}") return None def publish_config_update(self, update_data: Dict[str, Any]) -> bool: """发布配置更新通知""" if not self._redis_client: logger.warning("Redis未连接,无法发布配置更新") return False try: update_data["version"] = self._config_version update_data["timestamp"] = datetime.now().isoformat() self._redis_client.publish("config_update", json.dumps(update_data)) logger.info(f"已发布配置更新: {update_data}") return True except Exception as e: logger.error(f"发布配置更新失败: {e}") return False def invalidate_cache(self, cache_key: str): """使指定缓存失效""" self._cache.delete(cache_key) logger.info(f"缓存已失效: {cache_key}") def invalidate_all_cache(self): """使所有缓存失效""" self._cache.clear() logger.info("所有缓存已失效") def get_cache_stats(self) -> Dict[str, Any]: """获取缓存统计信息""" return self._cache.get_stats() def get_health_status(self) -> Dict[str, Any]: """获取健康状态""" redis_healthy = False if self._redis_client: try: self._redis_client.ping() redis_healthy = True except Exception: pass return { "redis_connected": redis_healthy, "config_version": self._config_version, "cache_stats": self.get_cache_stats(), "subscription_active": ( self._pubsub_thread is not None and self._pubsub_thread.is_alive() ), } def close(self): """关闭管理器""" self.stop_config_subscription() if self._redis_client: if self._redis_pubsub: self._redis_pubsub.close() self._redis_client.close() logger.info("Redis连接已关闭") def get_config_sync_manager() -> ConfigSyncManager: """获取配置同步管理器单例""" return ConfigSyncManager()