import os import time import json import argparse from typing import List from vs_types import GroupConfig from gco_wrapper import GCOWrapper from logger_setup import setup_app_logger from sentinel import SentinelGCO from toolkit.vs_cloud_api import VSCloudApi def load_config(path: str): with open(path, "r", encoding="utf-8") as f: return json.load(f) def save_config(path: str, new_config): new_config_str = json.dumps(new_config, indent=2) with open(path, "w", encoding="utf-8") as f: f.write(new_config_str) def main(): # ===== 1️⃣ 命令行参数 ===== parser = argparse.ArgumentParser(description="Sentinel Runner") parser.add_argument( "-c", "--config", type=str, required=False, default="config/config_sentinel.json", help="Path to sentinel config.json" ) args = parser.parse_args() config_path = args.config CONF_NAME = 'COORDINATOR_SENTINEL' # ===== 2️⃣ logger ===== app_logger = setup_app_logger("Sentinel") app_logger.info("Sentinel Logger is ready!") # ===== 3️⃣ 读取配置 ===== cfg_data = load_config(config_path) current_version = cfg_data.get('version', 0) redis_conf = cfg_data.get('redis') groups_conf = cfg_data.get('group_list', []) wrappers: List[GCOWrapper] = [] # ===== 4️⃣ 启动 groups ===== for item in groups_conf: cfg = GroupConfig.from_json(item) # 初始只启动 enable=True 的组 if not cfg.enable: app_logger.info(f"Group [{cfg.identifier}] is disabled initially. Skipping.") continue app_logger.info(f"Starting wrapper for group [{cfg.identifier}]...") wrapper = GCOWrapper( gco_class=SentinelGCO, gco_cfg=cfg, redis_conf=redis_conf ) wrapper.load() wrapper.start() wrappers.append(wrapper) app_logger.info( f"Successfully started {len(wrappers)} Sentinel groups. Press Ctrl+C to stop." ) # ===== 5️⃣ keep alive & 热更新监听 ===== try: while True: time.sleep(30) # 每 3 秒检查一次文件状态 if not os.path.exists(config_path): continue try: new_cfg_data = VSCloudApi.Instance().get_dynamic_config(config_name=CONF_NAME) new_version = new_cfg_data.get('version') # 如果 API 返回的没有 version 字段,或者版本号没变,则跳过 if new_version == current_version: continue app_logger.info("Config file modification detected! Reloading configurations...") save_config(config_path, new_cfg_data) current_version = new_version redis_conf = new_cfg_data.get('redis') new_groups_conf = new_cfg_data.get('group_list', []) # 转换新配置为 {identifier: config_dict} 的字典格式,方便 O(1) 查找 new_cfg_dict = { item.get("identifier"): item for item in new_groups_conf if item.get("identifier") } # ---------------- A. 处理【参数热更新】与【删除/禁用组】 ---------------- surviving_wrappers = [] for wrapper in wrappers: current_id = wrapper.m_cfg.identifier if current_id in new_cfg_dict: new_group_cfg = GroupConfig.from_json(new_cfg_dict[current_id]) # 情况 1: 如果后端把这个组的 enable 改成了 false,视同删除,直接停掉 if not new_group_cfg.enable: app_logger.info(f"Group [{current_id}] disabled by backend. Stopping and removing...") wrapper.stop() else: # 情况 2: 依然启用,调用刚才实现的热更新接口透传参数 wrapper.update_config(new_group_cfg) surviving_wrappers.append(wrapper) else: # 情况 3: 这个组完全从 JSON 中被删除了 app_logger.info(f"Group [{current_id}] deleted from config. Stopping and removing...") wrapper.stop() # 更新当前正在运行的 wrappers 列表 wrappers = surviving_wrappers # ---------------- B. 处理【新增组】与【重新启用组】 ---------------- existing_ids = {w.m_cfg.identifier for w in wrappers} for new_id, item_data in new_cfg_dict.items(): # 发现新出现的 ID(全新添加的,或是从 enable: false 变成 enable: true 的) if new_id not in existing_ids: cfg = GroupConfig.from_json(item_data) # 只有启用的组才会被启动 if not cfg.enable: continue app_logger.info(f"Dynamically starting wrapper for NEW group [{cfg.identifier}]...") new_wrapper = GCOWrapper( gco_class=SentinelGCO, gco_cfg=cfg, redis_conf=redis_conf ) try: new_wrapper.load() new_wrapper.start() wrappers.append(new_wrapper) except Exception as e: app_logger.error(f"Failed to dynamically start new group [{cfg.identifier}]: {e}") except json.JSONDecodeError: # 捕获异常:防止后端写入文件的中间状态导致 JSON 格式错误而崩溃 app_logger.warning("Config file is currently invalid JSON (maybe still writing), skipping this reload.") except Exception as e: app_logger.error(f"Error while hot-reloading config: {e}") except KeyboardInterrupt: app_logger.info("Shutting down Sentinels...") for wrapper in wrappers: wrapper.stop() if __name__ == "__main__": main()