from __future__ import annotations import logging import threading from typing import Optional from .config import AppConfig from .storage import Storage from .util import atomic_write_json, parse_iso, to_iso, utcnow class StatusScheduler: def __init__(self, storage: Storage, config: AppConfig, logger: Optional[logging.Logger] = None) -> None: self._storage = storage self._config = config self._logger = logger or logging.getLogger("argus.master.scheduler") self._stop_event = threading.Event() self._thread = threading.Thread(target=self._run, name="status-scheduler", daemon=True) self._nodes_json_lock = threading.Lock() self._pending_nodes_json = threading.Event() def start(self) -> None: """启动后台线程,定期刷新节点状态与 nodes.json。""" if not self._thread.is_alive(): self._logger.info("Starting scheduler thread") self._thread.start() def stop(self) -> None: self._stop_event.set() self._pending_nodes_json.set() self._thread.join(timeout=5) def trigger_nodes_json_refresh(self) -> None: self._pending_nodes_json.set() def generate_nodes_json(self) -> None: with self._nodes_json_lock: online_nodes = self._storage.get_online_nodes() atomic_write_json(self._config.metric_nodes_json_path, online_nodes) self._logger.info("nodes.json updated", extra={"count": len(online_nodes)}) # ------------------------------------------------------------------ # internal loop # ------------------------------------------------------------------ def _run(self) -> None: # 确保启动时 nodes.json 会立即生成 self._pending_nodes_json.set() while not self._stop_event.is_set(): changed = self._reconcile_statuses() if changed or self._pending_nodes_json.is_set(): try: self.generate_nodes_json() finally: self._pending_nodes_json.clear() self._stop_event.wait(self._config.scheduler_interval_seconds) def _reconcile_statuses(self) -> bool: """根据 last_report 与当前时间对比,决定是否切换状态。""" any_status_changed = False now = utcnow() rows = self._storage.fetch_nodes_for_scheduler() for row in rows: node_id = row["id"] last_report_iso = row["last_report"] current_status = row["status"] last_report_dt = parse_iso(last_report_iso) if last_report_dt is None: # No report yet; treat as initialized until report arrives continue delta_seconds = (now - last_report_dt).total_seconds() new_status = current_status if delta_seconds > self._config.offline_threshold_seconds: new_status = "offline" elif delta_seconds <= self._config.online_threshold_seconds: new_status = "online" # Between thresholds: keep current status (sticky) if new_status != current_status: any_status_changed = True self._logger.info( "Updating node status", extra={ "node_id": node_id, "previous": current_status, "new": new_status, "delta_seconds": delta_seconds, }, ) self._storage.update_status(node_id, new_status, last_updated_iso=to_iso(now)) return any_status_changed