97 lines
2.9 KiB
Python
97 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List
|
|
|
|
from .config import DeviceConfig
|
|
|
|
|
|
@dataclass
|
|
class DeviceRuntimeState:
|
|
cfg: DeviceConfig
|
|
next_scrape_at: float
|
|
consecutive_failures: int = 0
|
|
backoff_factor: int = 1
|
|
|
|
|
|
class DeviceRegistry:
|
|
"""维护设备配置与运行时调度状态."""
|
|
|
|
def __init__(self, global_scrape_interval: int = 60) -> None:
|
|
self._lock = threading.RLock()
|
|
self._states: Dict[str, DeviceRuntimeState] = {}
|
|
self._global_scrape_interval = global_scrape_interval
|
|
|
|
# --- 注册与删除 ---
|
|
|
|
def register_static_device(self, cfg: DeviceConfig) -> None:
|
|
self._register_device(cfg)
|
|
|
|
def register_runtime_device(self, cfg: DeviceConfig) -> None:
|
|
cfg.source = "runtime"
|
|
self._register_device(cfg)
|
|
|
|
def _register_device(self, cfg: DeviceConfig) -> None:
|
|
now = time.time()
|
|
with self._lock:
|
|
interval = cfg.scrape_interval_seconds or self._global_scrape_interval
|
|
state = DeviceRuntimeState(
|
|
cfg=cfg,
|
|
next_scrape_at=now, # 初始立即可采集
|
|
)
|
|
self._states[cfg.name] = state
|
|
|
|
def delete_runtime_device(self, name: str) -> None:
|
|
with self._lock:
|
|
state = self._states.get(name)
|
|
if state is not None and state.cfg.source == "runtime":
|
|
del self._states[name]
|
|
|
|
# --- 查询 ---
|
|
|
|
def list_devices(self) -> List[DeviceConfig]:
|
|
with self._lock:
|
|
return [s.cfg for s in self._states.values()]
|
|
|
|
def get_enabled_devices(self, now: float) -> List[DeviceRuntimeState]:
|
|
with self._lock:
|
|
return [
|
|
s
|
|
for s in self._states.values()
|
|
if s.cfg.enabled and s.next_scrape_at <= now
|
|
]
|
|
|
|
# --- 调度更新 ---
|
|
|
|
def update_after_scrape(
|
|
self,
|
|
device_name: str,
|
|
success: bool,
|
|
now: float,
|
|
scrape_interval: int,
|
|
failure_threshold: int,
|
|
max_backoff_factor: int,
|
|
) -> None:
|
|
"""Scraper 在单次采集后调用,更新运行状态."""
|
|
with self._lock:
|
|
state = self._states.get(device_name)
|
|
if state is None:
|
|
return
|
|
|
|
interval = state.cfg.scrape_interval_seconds or scrape_interval
|
|
|
|
if success:
|
|
state.consecutive_failures = 0
|
|
state.backoff_factor = 1
|
|
state.next_scrape_at = now + interval
|
|
else:
|
|
state.consecutive_failures += 1
|
|
if state.consecutive_failures >= failure_threshold:
|
|
state.backoff_factor = min(
|
|
max_backoff_factor, max(2, state.backoff_factor * 2)
|
|
)
|
|
state.next_scrape_at = now + interval * state.backoff_factor
|
|
|