main_sentinel.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import os
  2. import time
  3. import json
  4. import argparse
  5. from typing import List
  6. from vs_types import GroupConfig
  7. from gco_wrapper import GCOWrapper
  8. from logger_setup import setup_app_logger
  9. from sentinel import SentinelGCO
  10. from toolkit.vs_cloud_api import VSCloudApi
  11. def load_config(path: str):
  12. with open(path, "r", encoding="utf-8") as f:
  13. return json.load(f)
  14. def save_config(path: str, new_config):
  15. new_config_str = json.dumps(new_config, indent=2)
  16. with open(path, "w", encoding="utf-8") as f:
  17. f.write(new_config_str)
  18. def main():
  19. # ===== 1️⃣ 命令行参数 =====
  20. parser = argparse.ArgumentParser(description="Sentinel Runner")
  21. parser.add_argument(
  22. "-c", "--config",
  23. type=str,
  24. required=False,
  25. default="config/config_sentinel.json",
  26. help="Path to sentinel config.json"
  27. )
  28. args = parser.parse_args()
  29. config_path = args.config
  30. CONF_NAME = 'COORDINATOR_SENTINEL'
  31. # ===== 2️⃣ logger =====
  32. app_logger = setup_app_logger("Sentinel")
  33. app_logger.info("Sentinel Logger is ready!")
  34. # ===== 3️⃣ 读取配置 =====
  35. cfg_data = load_config(config_path)
  36. current_version = cfg_data.get('version', 0)
  37. redis_conf = cfg_data.get('redis')
  38. groups_conf = cfg_data.get('group_list', [])
  39. wrappers: List[GCOWrapper] = []
  40. # ===== 4️⃣ 启动 groups =====
  41. for item in groups_conf:
  42. cfg = GroupConfig.from_json(item)
  43. # 初始只启动 enable=True 的组
  44. if not cfg.enable:
  45. app_logger.info(f"Group [{cfg.identifier}] is disabled initially. Skipping.")
  46. continue
  47. app_logger.info(f"Starting wrapper for group [{cfg.identifier}]...")
  48. wrapper = GCOWrapper(
  49. gco_class=SentinelGCO,
  50. gco_cfg=cfg,
  51. redis_conf=redis_conf
  52. )
  53. wrapper.load()
  54. wrapper.start()
  55. wrappers.append(wrapper)
  56. app_logger.info(
  57. f"Successfully started {len(wrappers)} Sentinel groups. Press Ctrl+C to stop."
  58. )
  59. # ===== 5️⃣ keep alive & 热更新监听 =====
  60. try:
  61. while True:
  62. time.sleep(30) # 每 3 秒检查一次文件状态
  63. if not os.path.exists(config_path):
  64. continue
  65. try:
  66. new_cfg_data = VSCloudApi.Instance().get_dynamic_config(config_name=CONF_NAME)
  67. new_version = new_cfg_data.get('version')
  68. # 如果 API 返回的没有 version 字段,或者版本号没变,则跳过
  69. if new_version == current_version:
  70. continue
  71. app_logger.info("Config file modification detected! Reloading configurations...")
  72. save_config(config_path, new_cfg_data)
  73. current_version = new_version
  74. redis_conf = new_cfg_data.get('redis')
  75. new_groups_conf = new_cfg_data.get('group_list', [])
  76. # 转换新配置为 {identifier: config_dict} 的字典格式,方便 O(1) 查找
  77. new_cfg_dict = {
  78. item.get("identifier"): item
  79. for item in new_groups_conf
  80. if item.get("identifier")
  81. }
  82. # ---------------- A. 处理【参数热更新】与【删除/禁用组】 ----------------
  83. surviving_wrappers = []
  84. for wrapper in wrappers:
  85. current_id = wrapper.m_cfg.identifier
  86. if current_id in new_cfg_dict:
  87. new_group_cfg = GroupConfig.from_json(new_cfg_dict[current_id])
  88. # 情况 1: 如果后端把这个组的 enable 改成了 false,视同删除,直接停掉
  89. if not new_group_cfg.enable:
  90. app_logger.info(f"Group [{current_id}] disabled by backend. Stopping and removing...")
  91. wrapper.stop()
  92. else:
  93. # 情况 2: 依然启用,调用刚才实现的热更新接口透传参数
  94. wrapper.update_config(new_group_cfg)
  95. surviving_wrappers.append(wrapper)
  96. else:
  97. # 情况 3: 这个组完全从 JSON 中被删除了
  98. app_logger.info(f"Group [{current_id}] deleted from config. Stopping and removing...")
  99. wrapper.stop()
  100. # 更新当前正在运行的 wrappers 列表
  101. wrappers = surviving_wrappers
  102. # ---------------- B. 处理【新增组】与【重新启用组】 ----------------
  103. existing_ids = {w.m_cfg.identifier for w in wrappers}
  104. for new_id, item_data in new_cfg_dict.items():
  105. # 发现新出现的 ID(全新添加的,或是从 enable: false 变成 enable: true 的)
  106. if new_id not in existing_ids:
  107. cfg = GroupConfig.from_json(item_data)
  108. # 只有启用的组才会被启动
  109. if not cfg.enable:
  110. continue
  111. app_logger.info(f"Dynamically starting wrapper for NEW group [{cfg.identifier}]...")
  112. new_wrapper = GCOWrapper(
  113. gco_class=SentinelGCO,
  114. gco_cfg=cfg,
  115. redis_conf=redis_conf
  116. )
  117. try:
  118. new_wrapper.load()
  119. new_wrapper.start()
  120. wrappers.append(new_wrapper)
  121. except Exception as e:
  122. app_logger.error(f"Failed to dynamically start new group [{cfg.identifier}]: {e}")
  123. except json.JSONDecodeError:
  124. # 捕获异常:防止后端写入文件的中间状态导致 JSON 格式错误而崩溃
  125. app_logger.warning("Config file is currently invalid JSON (maybe still writing), skipping this reload.")
  126. except Exception as e:
  127. app_logger.error(f"Error while hot-reloading config: {e}")
  128. except KeyboardInterrupt:
  129. app_logger.info("Shutting down Sentinels...")
  130. for wrapper in wrappers:
  131. wrapper.stop()
  132. if __name__ == "__main__":
  133. main()