main_sentinel.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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. if not os.path.exists(path):
  13. return {}
  14. with open(path, "r", encoding="utf-8") as f:
  15. return json.load(f)
  16. def save_config(path: str, new_config):
  17. new_config_str = json.dumps(new_config, indent=2)
  18. with open(path, "w", encoding="utf-8") as f:
  19. f.write(new_config_str)
  20. def main():
  21. # ===== 1️⃣ 命令行参数 =====
  22. parser = argparse.ArgumentParser(description="Sentinel Runner")
  23. parser.add_argument(
  24. "-c", "--config",
  25. type=str,
  26. required=False,
  27. default="config/config_sentinel.json",
  28. help="Path to sentinel config.json"
  29. )
  30. args = parser.parse_args()
  31. config_path = args.config
  32. CONF_NAME = 'COORDINATOR_SENTINEL'
  33. # ===== 2️⃣ logger =====
  34. app_logger = setup_app_logger("Sentinel")
  35. app_logger.info("Sentinel Logger is ready!")
  36. # ===== 3️⃣ 读取配置 =====
  37. cfg_data = load_config(config_path)
  38. current_version = cfg_data.get('version', 0)
  39. redis_conf = cfg_data.get('redis')
  40. groups_conf = cfg_data.get('group_list', [])
  41. wrappers: List[GCOWrapper] = []
  42. # ===== 4️⃣ 启动 groups =====
  43. for item in groups_conf:
  44. cfg = GroupConfig.from_json(item)
  45. # 初始只启动 enable=True 的组
  46. if not cfg.enable:
  47. app_logger.info(f"Group [{cfg.identifier}] is disabled initially. Skipping.")
  48. continue
  49. app_logger.info(f"Starting wrapper for group [{cfg.identifier}]...")
  50. wrapper = GCOWrapper(
  51. gco_class=SentinelGCO,
  52. gco_cfg=cfg,
  53. redis_conf=redis_conf
  54. )
  55. wrapper.load()
  56. wrapper.start()
  57. wrappers.append(wrapper)
  58. app_logger.info(
  59. f"Successfully started {len(wrappers)} Sentinel groups. Press Ctrl+C to stop."
  60. )
  61. # ===== 5️⃣ keep alive & 热更新监听 =====
  62. try:
  63. while True:
  64. time.sleep(30) # 每 3 秒检查一次文件状态
  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()