sentinel.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import os
  2. import time
  3. import json
  4. import random
  5. import threading
  6. from datetime import datetime
  7. from typing import List, Dict, Callable
  8. from vs_types import GroupConfig, VSPlgConfig, Task, QueryWaitMode
  9. from vs_plg_factory import VSPlgFactory
  10. from toolkit.vs_cloud_api import VSCloudApi
  11. from utils.safe_redis_cli import SafeRedisClient
  12. class SentinelGCO:
  13. def __init__(self, cfg: GroupConfig, redis_conf: Dict, logger: Callable[[str], None] = None):
  14. self.m_cfg = cfg
  15. self.m_factory = VSPlgFactory()
  16. self.m_logger = logger
  17. self.m_tasks: List[Task] = []
  18. self.m_lock = threading.RLock()
  19. self.m_stop_event = threading.Event()
  20. self.redis_client = SafeRedisClient(redis_conf, self.m_logger)
  21. self.m_last_group_query_time = 0.0
  22. def _log(self, message):
  23. if self.m_logger:
  24. self.m_logger(f'[SENTINEL] [{self.m_cfg.identifier}] {message}')
  25. else:
  26. print(f'[SENTINEL] [{self.m_cfg.identifier}] {message}')
  27. def _get_average_interval(self) -> float:
  28. """计算当前组平均的查询间隔(秒)"""
  29. mode = self.m_cfg.sentinel.query_wait.mode
  30. if mode == QueryWaitMode.Loop:
  31. return 1.0
  32. elif mode == QueryWaitMode.Fixed:
  33. return float(self.m_cfg.sentinel.query_wait.fixed_wait)
  34. elif mode == QueryWaitMode.Random:
  35. return (self.m_cfg.sentinel.query_wait.random_min + self.m_cfg.sentinel.query_wait.random_max) / 2.0
  36. return 30.0
  37. def update_config(self, new_cfg: GroupConfig):
  38. """
  39. 动态更新配置
  40. """
  41. with self.m_lock:
  42. if self.m_cfg.enable and not new_cfg.enable:
  43. self._log("Config dynamically updated: Group DISABLED. Will stop creating new tasks.")
  44. elif not self.m_cfg.enable and new_cfg.enable:
  45. self._log("Config dynamically updated: Group ENABLED.")
  46. else:
  47. self._log("Config dynamically updated: Parameters refreshed.")
  48. self.m_cfg = new_cfg
  49. def start(self):
  50. if not self.m_cfg.enable:
  51. return
  52. self._log("Starting Sentinel...")
  53. plugin_name = self.m_cfg.plugin_config.plugin_name
  54. class_name = "".join(part.title() for part in plugin_name.split('_'))
  55. plugin_path = os.path.join(self.m_cfg.plugin_config.lib_path, self.m_cfg.plugin_config.plugin_bin)
  56. self.m_factory.register_plugin(plugin_name, plugin_path, class_name)
  57. threading.Thread(target=self._monitor_loop, daemon=True, name="Sentinel-Monitor").start()
  58. threading.Thread(target=self._creator_loop, daemon=True, name="Sentinel-Creator").start()
  59. def stop(self):
  60. self._log("Stopping Sentinel...")
  61. self.m_stop_event.set()
  62. with self.m_lock:
  63. tasks_to_cleanup = list(self.m_tasks)
  64. self.m_tasks.clear()
  65. for task in tasks_to_cleanup:
  66. self._cleanup_task(task, "sentinel stopped")
  67. def _cleanup_task(self, task: Task, reason: str):
  68. try:
  69. if task and task.instance:
  70. self._log(f"Cleaning up sentinel instance. reason={reason}")
  71. task.instance.cleanup()
  72. except Exception as e:
  73. self._log(f"Cleanup failed. reason={reason}, error={e}")
  74. def _remove_task(self, task: Task, reason: str):
  75. removed = False
  76. with self.m_lock:
  77. if task in self.m_tasks:
  78. self.m_tasks.remove(task)
  79. removed = True
  80. if removed:
  81. self._cleanup_task(task, reason)
  82. def _get_redis_key(self, routing_key: str) -> str:
  83. return f"vs:signal:{routing_key}"
  84. def _is_within_active_hours(self) -> bool:
  85. """
  86. 判断当前是否在允许创建实例的时间段内
  87. """
  88. start_str = self.m_cfg.active_time_start
  89. end_str = self.m_cfg.active_time_end
  90. current_bj_time = datetime.now().time()
  91. start_time = datetime.strptime(start_str, "%H:%M").time()
  92. end_time = datetime.strptime(end_str, "%H:%M").time()
  93. return start_time <= current_bj_time <= end_time
  94. def _monitor_loop(self):
  95. self._log("Monitor loop started.")
  96. self.m_last_group_query_time = 0.0
  97. while not self.m_stop_event.is_set():
  98. try:
  99. time.sleep(1)
  100. now = time.time()
  101. with self.m_lock:
  102. tasks_to_check = list(self.m_tasks)
  103. active_tasks = []
  104. dead_tasks = []
  105. for t in tasks_to_check:
  106. if t.instance.health_check():
  107. active_tasks.append(t)
  108. else:
  109. dead_tasks.append(t)
  110. if dead_tasks:
  111. with self.m_lock:
  112. current_tasks = list(self.m_tasks)
  113. self.m_tasks = [t for t in self.m_tasks if t in active_tasks]
  114. for t in dead_tasks:
  115. if t in current_tasks:
  116. self._cleanup_task(t, "health check failed")
  117. else:
  118. with self.m_lock:
  119. self.m_tasks = [t for t in self.m_tasks if t in active_tasks]
  120. if not active_tasks:
  121. continue
  122. avg_interval = self._get_average_interval()
  123. global_gap = max(1.0, avg_interval / len(active_tasks))
  124. active_tasks.sort(key=lambda x: x.next_run)
  125. for task in active_tasks:
  126. if now < task.next_run:
  127. continue
  128. if now - self.m_last_group_query_time < global_gap:
  129. break
  130. apt_types = self.m_cfg.appointment_types
  131. if not apt_types:
  132. continue
  133. weights = [float(item.weight) for item in apt_types]
  134. apt_type = random.choices(apt_types, weights=weights, k=1)[0]
  135. interval = 30
  136. mode = task.qw_cfg.mode
  137. if mode == QueryWaitMode.Loop:
  138. interval = 1
  139. elif mode == QueryWaitMode.Fixed:
  140. interval = task.qw_cfg.fixed_wait
  141. elif mode == QueryWaitMode.Random:
  142. interval = random.randint(task.qw_cfg.random_min, task.qw_cfg.random_max)
  143. self.m_last_group_query_time = now
  144. try:
  145. VSCloudApi.Instance().slot_refresh_start(
  146. apt_type.routing_key,
  147. country=apt_type.country,
  148. city=apt_type.city,
  149. visa_type=apt_type.visa_type
  150. )
  151. result = task.instance.query(apt_type)
  152. result.apt_type = apt_type
  153. if result.success:
  154. ttl = self.m_cfg.sentinel.signal_ttl
  155. self._log(f"🔥 SLOT FOUND! Writing signal to Redis (TTL: {ttl}s)")
  156. payload = {
  157. "group_id": self.m_cfg.identifier,
  158. "apt_type": apt_type.model_dump(),
  159. "query_result": result.to_snapshot_payload(),
  160. "timestamp": time.time()
  161. }
  162. redis_key = self._get_redis_key(apt_type.routing_key)
  163. self.redis_client.setex(redis_key, ttl, json.dumps(payload))
  164. payload["query_result"]["website"] = self.m_cfg.website
  165. VSCloudApi.Instance().slot_snapshot_report(payload["query_result"])
  166. VSCloudApi.Instance().slot_refresh_success(apt_type.routing_key)
  167. except Exception as e:
  168. self._log(f"Query exception: {e}")
  169. VSCloudApi.Instance().slot_refresh_fail(apt_type.routing_key, error=str(e))
  170. finally:
  171. task.next_run = time.time() + interval
  172. break
  173. except Exception as e:
  174. self._log(f"Monitor loop error: {e}")
  175. def _creator_loop(self):
  176. self._log("Creator loop started.")
  177. while not self.m_stop_event.wait(1.0):
  178. try:
  179. if not self._is_within_active_hours():
  180. continue
  181. with self.m_lock:
  182. current = len(self.m_tasks)
  183. target = self.m_cfg.sentinel.target_instances
  184. if current < target:
  185. self._spawn_sentinel_worker()
  186. except Exception as e:
  187. self._log(f'Creator loop exception: {e}')
  188. def _spawn_sentinel_worker(self):
  189. instance = None
  190. success = False
  191. plg_cfg = None
  192. try:
  193. plg_cfg = VSPlgConfig()
  194. plg_cfg.debug = self.m_cfg.debug
  195. plg_cfg.free_config = self.m_cfg.free_config
  196. plg_cfg.session_max_life = self.m_cfg.session_max_life
  197. if not self.m_cfg.need_account:
  198. plg_cfg.account.id = 0
  199. plg_cfg.account.username = "Guest"
  200. else:
  201. acc = VSCloudApi.Instance().get_next_account(self.m_cfg.sentinel.account_pool_id, self.m_cfg.sentinel.account_cd)
  202. plg_cfg.account = type(plg_cfg.account)(**acc)
  203. if self.m_cfg.need_proxy:
  204. proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
  205. plg_cfg.proxy = type(plg_cfg.proxy)(**proxy)
  206. instance = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
  207. instance.set_log(self.m_logger)
  208. instance.set_config(plg_cfg)
  209. instance.create_session()
  210. with self.m_lock:
  211. self.m_tasks.append(
  212. Task(instance=instance, qw_cfg=self.m_cfg.sentinel.query_wait, next_run=time.time(), book_allowed=False)
  213. )
  214. success = True
  215. self._log(f"+++ Sentinel spawned: {plg_cfg.account.username}")
  216. except Exception as e:
  217. err_str = str(e)
  218. self._log(f"Spawn failed: {err_str}")
  219. rate_limited_indicators = [
  220. "42901" in err_str,
  221. "Rate limited" in err_str
  222. ]
  223. if any(rate_limited_indicators):
  224. if plg_cfg and plg_cfg.account.username != "Guest":
  225. VSCloudApi.lock_account(plg_cfg.account.id, self.m_cfg.login_backoff)
  226. finally:
  227. if not success and instance is not None:
  228. instance.cleanup()