health.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. # -*- coding: utf-8 -*-
  2. """
  3. Health-Check für Verbindungen.
  4. Bietet Ping-basierte und protokollspezifische Health-Checks.
  5. """
  6. import asyncio
  7. import time
  8. from dataclasses import dataclass
  9. from enum import IntEnum, auto
  10. from typing import Callable, Awaitable, Any
  11. from trixy_core.network.pool.connection import Connection
  12. class HealthStatus(IntEnum):
  13. """Gesundheitszustand einer Verbindung."""
  14. HEALTHY = auto() # Verbindung ist gesund
  15. DEGRADED = auto() # Verbindung funktioniert, aber langsam
  16. UNHEALTHY = auto() # Verbindung ist nicht gesund
  17. UNKNOWN = auto() # Status unbekannt
  18. @dataclass
  19. class HealthCheckConfig:
  20. """
  21. Konfiguration für Health-Checks.
  22. Attributes:
  23. interval: Prüfungsintervall in Sekunden
  24. timeout: Timeout für einzelne Prüfung
  25. healthy_threshold: Anzahl erfolgreicher Prüfungen für HEALTHY
  26. unhealthy_threshold: Anzahl fehlgeschlagener Prüfungen für UNHEALTHY
  27. degraded_latency: Latenz ab der DEGRADED gemeldet wird
  28. """
  29. interval: float = 30.0
  30. timeout: float = 5.0
  31. healthy_threshold: int = 2
  32. unhealthy_threshold: int = 3
  33. degraded_latency: float = 1.0 # 1 Sekunde
  34. @dataclass
  35. class HealthCheckResult:
  36. """Ergebnis eines Health-Checks."""
  37. status: HealthStatus
  38. latency: float
  39. message: str = ""
  40. timestamp: float = 0.0
  41. def __post_init__(self) -> None:
  42. if self.timestamp == 0.0:
  43. self.timestamp = time.time()
  44. # Type alias für Health-Check-Funktionen
  45. HealthCheckFunc = Callable[[Connection], Awaitable[HealthCheckResult]]
  46. class HealthChecker:
  47. """
  48. Health-Checker für Verbindungen.
  49. Führt periodische Gesundheitsprüfungen durch und verfolgt den Status.
  50. Example:
  51. checker = HealthChecker(HealthCheckConfig(
  52. interval=30.0,
  53. timeout=5.0
  54. ))
  55. # Custom-Check registrieren
  56. async def ping_check(conn: Connection) -> HealthCheckResult:
  57. start = time.monotonic()
  58. await conn.write(b"PING\\n")
  59. response = await conn.readline()
  60. latency = time.monotonic() - start
  61. if response == b"PONG\\n":
  62. return HealthCheckResult(HealthStatus.HEALTHY, latency)
  63. return HealthCheckResult(HealthStatus.UNHEALTHY, latency, "Invalid response")
  64. checker.set_check_function(ping_check)
  65. # Einzelprüfung
  66. result = await checker.check(connection)
  67. print(f"Status: {result.status.name}, Latenz: {result.latency*1000:.2f}ms")
  68. """
  69. def __init__(self, config: HealthCheckConfig | None = None) -> None:
  70. """
  71. Initialisiert den Health-Checker.
  72. Args:
  73. config: Health-Check-Konfiguration
  74. """
  75. self._config = config or HealthCheckConfig()
  76. self._check_func: HealthCheckFunc | None = None
  77. self._history: dict[str, list[HealthCheckResult]] = {}
  78. self._status: dict[str, HealthStatus] = {}
  79. def set_check_function(self, func: HealthCheckFunc) -> None:
  80. """
  81. Setzt die Health-Check-Funktion.
  82. Args:
  83. func: Async-Funktion die einen HealthCheckResult zurückgibt
  84. """
  85. self._check_func = func
  86. async def check(self, conn: Connection) -> HealthCheckResult:
  87. """
  88. Führt einen Health-Check durch.
  89. Args:
  90. conn: Die zu prüfende Verbindung
  91. Returns:
  92. HealthCheckResult
  93. """
  94. if self._check_func:
  95. try:
  96. result = await asyncio.wait_for(
  97. self._check_func(conn),
  98. timeout=self._config.timeout
  99. )
  100. except asyncio.TimeoutError:
  101. result = HealthCheckResult(
  102. status=HealthStatus.UNHEALTHY,
  103. latency=self._config.timeout,
  104. message="Health-Check Timeout"
  105. )
  106. except Exception as e:
  107. result = HealthCheckResult(
  108. status=HealthStatus.UNHEALTHY,
  109. latency=0.0,
  110. message=str(e)
  111. )
  112. else:
  113. # Standard-Check: Socket-Prüfung
  114. result = await self._default_check(conn)
  115. # Historie aktualisieren
  116. self._update_history(conn.id, result)
  117. return result
  118. async def _default_check(self, conn: Connection) -> HealthCheckResult:
  119. """Standard Health-Check basierend auf Socket-Status."""
  120. start = time.monotonic()
  121. try:
  122. # Prüfe ob Verbindung noch offen
  123. if conn.at_eof():
  124. return HealthCheckResult(
  125. status=HealthStatus.UNHEALTHY,
  126. latency=0.0,
  127. message="Connection EOF"
  128. )
  129. # Prüfe Socket-Status
  130. if not conn.is_usable:
  131. return HealthCheckResult(
  132. status=HealthStatus.UNHEALTHY,
  133. latency=0.0,
  134. message=f"Connection state: {conn.state.name}"
  135. )
  136. latency = time.monotonic() - start
  137. # Latenz-basierte Bewertung
  138. if latency > self._config.degraded_latency:
  139. return HealthCheckResult(
  140. status=HealthStatus.DEGRADED,
  141. latency=latency,
  142. message="High latency"
  143. )
  144. return HealthCheckResult(
  145. status=HealthStatus.HEALTHY,
  146. latency=latency
  147. )
  148. except Exception as e:
  149. return HealthCheckResult(
  150. status=HealthStatus.UNHEALTHY,
  151. latency=time.monotonic() - start,
  152. message=str(e)
  153. )
  154. def _update_history(self, conn_id: str, result: HealthCheckResult) -> None:
  155. """Aktualisiert die Check-Historie."""
  156. if conn_id not in self._history:
  157. self._history[conn_id] = []
  158. history = self._history[conn_id]
  159. history.append(result)
  160. # Nur letzte N Ergebnisse behalten
  161. max_history = max(
  162. self._config.healthy_threshold,
  163. self._config.unhealthy_threshold
  164. ) * 2
  165. if len(history) > max_history:
  166. history[:] = history[-max_history:]
  167. # Status aktualisieren
  168. self._update_status(conn_id)
  169. def _update_status(self, conn_id: str) -> None:
  170. """Aktualisiert den aggregierten Status."""
  171. history = self._history.get(conn_id, [])
  172. if not history:
  173. self._status[conn_id] = HealthStatus.UNKNOWN
  174. return
  175. # Zähle letzte Ergebnisse
  176. recent = history[-max(
  177. self._config.healthy_threshold,
  178. self._config.unhealthy_threshold
  179. ):]
  180. healthy_count = sum(
  181. 1 for r in recent if r.status == HealthStatus.HEALTHY
  182. )
  183. unhealthy_count = sum(
  184. 1 for r in recent if r.status == HealthStatus.UNHEALTHY
  185. )
  186. if unhealthy_count >= self._config.unhealthy_threshold:
  187. self._status[conn_id] = HealthStatus.UNHEALTHY
  188. elif healthy_count >= self._config.healthy_threshold:
  189. self._status[conn_id] = HealthStatus.HEALTHY
  190. else:
  191. # Prüfe auf DEGRADED
  192. degraded_count = sum(
  193. 1 for r in recent if r.status == HealthStatus.DEGRADED
  194. )
  195. if degraded_count > 0:
  196. self._status[conn_id] = HealthStatus.DEGRADED
  197. else:
  198. self._status[conn_id] = HealthStatus.UNKNOWN
  199. def get_status(self, conn_id: str) -> HealthStatus:
  200. """
  201. Gibt den aggregierten Status einer Verbindung zurück.
  202. Args:
  203. conn_id: Verbindungs-ID
  204. Returns:
  205. Aggregierter HealthStatus
  206. """
  207. return self._status.get(conn_id, HealthStatus.UNKNOWN)
  208. def get_history(self, conn_id: str) -> list[HealthCheckResult]:
  209. """
  210. Gibt die Check-Historie einer Verbindung zurück.
  211. Args:
  212. conn_id: Verbindungs-ID
  213. Returns:
  214. Liste der letzten Ergebnisse
  215. """
  216. return self._history.get(conn_id, []).copy()
  217. def get_average_latency(self, conn_id: str, samples: int = 10) -> float:
  218. """
  219. Berechnet die durchschnittliche Latenz.
  220. Args:
  221. conn_id: Verbindungs-ID
  222. samples: Anzahl zu berücksichtigender Samples
  223. Returns:
  224. Durchschnittliche Latenz in Sekunden
  225. """
  226. history = self._history.get(conn_id, [])
  227. if not history:
  228. return 0.0
  229. recent = history[-samples:]
  230. latencies = [r.latency for r in recent if r.latency > 0]
  231. if not latencies:
  232. return 0.0
  233. return sum(latencies) / len(latencies)
  234. def clear(self, conn_id: str | None = None) -> None:
  235. """
  236. Löscht die Historie.
  237. Args:
  238. conn_id: Optional spezifische Verbindung, sonst alle
  239. """
  240. if conn_id:
  241. self._history.pop(conn_id, None)
  242. self._status.pop(conn_id, None)
  243. else:
  244. self._history.clear()
  245. self._status.clear()
  246. def get_statistics(self) -> dict[str, Any]:
  247. """Gibt Statistiken zurück."""
  248. total = len(self._status)
  249. healthy = sum(1 for s in self._status.values() if s == HealthStatus.HEALTHY)
  250. degraded = sum(1 for s in self._status.values() if s == HealthStatus.DEGRADED)
  251. unhealthy = sum(1 for s in self._status.values() if s == HealthStatus.UNHEALTHY)
  252. return {
  253. "total_connections": total,
  254. "healthy": healthy,
  255. "degraded": degraded,
  256. "unhealthy": unhealthy,
  257. "unknown": total - healthy - degraded - unhealthy
  258. }
  259. class PeriodicHealthChecker:
  260. """
  261. Periodischer Health-Checker für mehrere Verbindungen.
  262. Führt automatisch Health-Checks in regelmäßigen Intervallen durch.
  263. """
  264. def __init__(
  265. self,
  266. checker: HealthChecker,
  267. on_unhealthy: Callable[[Connection], Awaitable[None]] | None = None
  268. ) -> None:
  269. """
  270. Initialisiert den periodischen Checker.
  271. Args:
  272. checker: Der zu verwendende HealthChecker
  273. on_unhealthy: Callback bei unhealthy Verbindung
  274. """
  275. self._checker = checker
  276. self._on_unhealthy = on_unhealthy
  277. self._connections: list[Connection] = []
  278. self._task: asyncio.Task | None = None
  279. self._running = False
  280. def add_connection(self, conn: Connection) -> None:
  281. """Fügt eine Verbindung zur Überwachung hinzu."""
  282. if conn not in self._connections:
  283. self._connections.append(conn)
  284. def remove_connection(self, conn: Connection) -> None:
  285. """Entfernt eine Verbindung von der Überwachung."""
  286. if conn in self._connections:
  287. self._connections.remove(conn)
  288. self._checker.clear(conn.id)
  289. async def start(self) -> None:
  290. """Startet die periodischen Checks."""
  291. if self._running:
  292. return
  293. self._running = True
  294. self._task = asyncio.create_task(self._check_loop())
  295. async def stop(self) -> None:
  296. """Stoppt die periodischen Checks."""
  297. self._running = False
  298. if self._task:
  299. self._task.cancel()
  300. try:
  301. await self._task
  302. except asyncio.CancelledError:
  303. pass
  304. self._task = None
  305. async def _check_loop(self) -> None:
  306. """Hauptschleife für periodische Checks."""
  307. while self._running:
  308. try:
  309. await asyncio.sleep(self._checker._config.interval)
  310. for conn in list(self._connections):
  311. try:
  312. result = await self._checker.check(conn)
  313. if result.status == HealthStatus.UNHEALTHY:
  314. if self._on_unhealthy:
  315. await self._on_unhealthy(conn)
  316. except Exception:
  317. pass
  318. except asyncio.CancelledError:
  319. break
  320. except Exception:
  321. pass