least_connections.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. # -*- coding: utf-8 -*-
  2. """
  3. Least-Connections Load-Balancing-Strategie.
  4. Wählt den Satellite mit den wenigsten aktiven Verbindungen/Anfragen.
  5. """
  6. from typing import Any, TYPE_CHECKING
  7. from trixy_core.satellite.loadbalancing.strategy import (
  8. LoadBalancingStrategy,
  9. SelectionResult,
  10. )
  11. if TYPE_CHECKING:
  12. from trixy_core.satellite.satellite import Satellite
  13. class LeastConnectionsStrategy(LoadBalancingStrategy):
  14. """
  15. Least-Connections-Strategie.
  16. Wählt immer den Satellite mit den wenigsten aktiven Anfragen.
  17. Ideal für Anfragen mit unterschiedlicher Bearbeitungsdauer.
  18. Example:
  19. strategy = LeastConnectionsStrategy()
  20. balancer = LoadBalancer(strategy)
  21. result = balancer.select(satellites)
  22. satellite = result.satellite
  23. # Anfrage starten (erhöht Connection-Count)
  24. await satellite.speak("Hallo")
  25. # Anfrage beenden (verringert Connection-Count)
  26. balancer.end_request(satellite)
  27. """
  28. def __init__(self) -> None:
  29. """Initialisiert die Least-Connections-Strategie."""
  30. self._connections: dict[str, int] = {}
  31. @property
  32. def name(self) -> str:
  33. """Name der Strategie."""
  34. return "least_connections"
  35. def select(
  36. self,
  37. satellites: list["Satellite"],
  38. context: dict[str, Any] | None = None,
  39. ) -> SelectionResult | None:
  40. """
  41. Wählt den Satellite mit den wenigsten Verbindungen.
  42. Args:
  43. satellites: Liste verfügbarer Satellites
  44. context: Optionaler Kontext
  45. Returns:
  46. SelectionResult oder None
  47. """
  48. if not satellites:
  49. return None
  50. # Sortiere nach Verbindungen (aufsteigend), dann nach ID für Konsistenz
  51. sorted_satellites = sorted(
  52. satellites,
  53. key=lambda s: (self.get_connection_count(s.id), s.id)
  54. )
  55. selected = sorted_satellites[0]
  56. connections = self.get_connection_count(selected.id)
  57. return SelectionResult(
  58. satellite=selected,
  59. reason=f"Least connections ({connections})",
  60. score=1.0 / max(1, connections + 1),
  61. metadata={"connection_count": connections}
  62. )
  63. def get_connection_count(self, satellite_id: str) -> int:
  64. """
  65. Gibt die Anzahl aktiver Verbindungen zurück.
  66. Args:
  67. satellite_id: Satellite-ID
  68. Returns:
  69. Anzahl Verbindungen
  70. """
  71. return self._connections.get(satellite_id, 0)
  72. def on_request_start(self, satellite: "Satellite") -> None:
  73. """Erhöht den Connection-Count."""
  74. self._connections[satellite.id] = self.get_connection_count(satellite.id) + 1
  75. def on_request_end(self, satellite: "Satellite", success: bool = True) -> None:
  76. """Verringert den Connection-Count."""
  77. current = self.get_connection_count(satellite.id)
  78. if current > 0:
  79. self._connections[satellite.id] = current - 1
  80. def reset(self) -> None:
  81. """Setzt alle Connection-Counts zurück."""
  82. self._connections.clear()
  83. def get_all_counts(self) -> dict[str, int]:
  84. """Gibt alle Connection-Counts zurück."""
  85. return self._connections.copy()
  86. class LeastResponseTimeStrategy(LoadBalancingStrategy):
  87. """
  88. Least-Response-Time-Strategie.
  89. Kombiniert Verbindungsanzahl mit durchschnittlicher Antwortzeit.
  90. Wählt den Satellite mit der niedrigsten erwarteten Wartezeit.
  91. Formel: score = connections * avg_response_time
  92. Example:
  93. strategy = LeastResponseTimeStrategy()
  94. balancer = LoadBalancer(strategy)
  95. result = balancer.select(satellites)
  96. satellite = result.satellite
  97. start_time = time.monotonic()
  98. await satellite.speak("Hallo")
  99. response_time = time.monotonic() - start_time
  100. strategy.record_response_time(satellite.id, response_time)
  101. balancer.end_request(satellite)
  102. """
  103. def __init__(self, window_size: int = 10) -> None:
  104. """
  105. Initialisiert die Strategie.
  106. Args:
  107. window_size: Anzahl der letzten Antwortzeiten für Durchschnitt
  108. """
  109. self._connections: dict[str, int] = {}
  110. self._response_times: dict[str, list[float]] = {}
  111. self._window_size = window_size
  112. @property
  113. def name(self) -> str:
  114. """Name der Strategie."""
  115. return "least_response_time"
  116. def record_response_time(self, satellite_id: str, response_time: float) -> None:
  117. """
  118. Zeichnet eine Antwortzeit auf.
  119. Args:
  120. satellite_id: Satellite-ID
  121. response_time: Antwortzeit in Sekunden
  122. """
  123. if satellite_id not in self._response_times:
  124. self._response_times[satellite_id] = []
  125. times = self._response_times[satellite_id]
  126. times.append(response_time)
  127. # Nur letzte N Werte behalten
  128. if len(times) > self._window_size:
  129. self._response_times[satellite_id] = times[-self._window_size:]
  130. def get_avg_response_time(self, satellite_id: str) -> float:
  131. """
  132. Gibt die durchschnittliche Antwortzeit zurück.
  133. Args:
  134. satellite_id: Satellite-ID
  135. Returns:
  136. Durchschnittliche Antwortzeit (Standard: 1.0)
  137. """
  138. times = self._response_times.get(satellite_id, [])
  139. if not times:
  140. return 1.0 # Standard-Annahme
  141. return sum(times) / len(times)
  142. def get_connection_count(self, satellite_id: str) -> int:
  143. """Gibt die Anzahl aktiver Verbindungen zurück."""
  144. return self._connections.get(satellite_id, 0)
  145. def _calculate_score(self, satellite_id: str) -> float:
  146. """Berechnet den Score für einen Satellite (niedriger = besser)."""
  147. connections = self.get_connection_count(satellite_id)
  148. avg_time = self.get_avg_response_time(satellite_id)
  149. # Formel: Aktuelle Last * durchschnittliche Zeit
  150. return (connections + 1) * avg_time
  151. def select(
  152. self,
  153. satellites: list["Satellite"],
  154. context: dict[str, Any] | None = None,
  155. ) -> SelectionResult | None:
  156. """
  157. Wählt den Satellite mit der niedrigsten erwarteten Wartezeit.
  158. Args:
  159. satellites: Liste verfügbarer Satellites
  160. context: Optionaler Kontext
  161. Returns:
  162. SelectionResult oder None
  163. """
  164. if not satellites:
  165. return None
  166. # Sortiere nach Score (aufsteigend)
  167. sorted_satellites = sorted(
  168. satellites,
  169. key=lambda s: (self._calculate_score(s.id), s.id)
  170. )
  171. selected = sorted_satellites[0]
  172. score = self._calculate_score(selected.id)
  173. return SelectionResult(
  174. satellite=selected,
  175. reason=f"Least response time (score={score:.3f})",
  176. score=1.0 / max(0.001, score),
  177. metadata={
  178. "connection_count": self.get_connection_count(selected.id),
  179. "avg_response_time": self.get_avg_response_time(selected.id),
  180. "calculated_score": score,
  181. }
  182. )
  183. def on_request_start(self, satellite: "Satellite") -> None:
  184. """Erhöht den Connection-Count."""
  185. self._connections[satellite.id] = self.get_connection_count(satellite.id) + 1
  186. def on_request_end(self, satellite: "Satellite", success: bool = True) -> None:
  187. """Verringert den Connection-Count."""
  188. current = self.get_connection_count(satellite.id)
  189. if current > 0:
  190. self._connections[satellite.id] = current - 1
  191. def reset(self) -> None:
  192. """Setzt den Zustand zurück."""
  193. self._connections.clear()
  194. self._response_times.clear()