strategy.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. # -*- coding: utf-8 -*-
  2. """
  3. Load-Balancing-Strategie Basisklassen.
  4. Definiert das Interface für alle Load-Balancing-Strategien.
  5. """
  6. from abc import ABC, abstractmethod
  7. from dataclasses import dataclass
  8. from typing import Any, TYPE_CHECKING
  9. if TYPE_CHECKING:
  10. from trixy_core.satellite.satellite import Satellite
  11. @dataclass
  12. class SelectionResult:
  13. """
  14. Ergebnis einer Satellite-Auswahl.
  15. Attributes:
  16. satellite: Der ausgewählte Satellite
  17. reason: Grund für die Auswahl
  18. score: Optionaler Score (für Weighted-Strategien)
  19. metadata: Zusätzliche Informationen
  20. """
  21. satellite: "Satellite"
  22. reason: str = ""
  23. score: float = 0.0
  24. metadata: dict[str, Any] | None = None
  25. class LoadBalancingStrategy(ABC):
  26. """
  27. Abstrakte Basisklasse für Load-Balancing-Strategien.
  28. Definiert das Interface für alle Strategien zur
  29. Auswahl eines Satellites aus einer Gruppe.
  30. Example:
  31. class MyStrategy(LoadBalancingStrategy):
  32. def select(self, satellites):
  33. # Eigene Logik
  34. return SelectionResult(satellites[0])
  35. """
  36. @property
  37. @abstractmethod
  38. def name(self) -> str:
  39. """Name der Strategie."""
  40. ...
  41. @abstractmethod
  42. def select(
  43. self,
  44. satellites: list["Satellite"],
  45. context: dict[str, Any] | None = None,
  46. ) -> SelectionResult | None:
  47. """
  48. Wählt einen Satellite aus der Liste aus.
  49. Args:
  50. satellites: Liste verfügbarer Satellites
  51. context: Optionaler Kontext für die Auswahl
  52. Returns:
  53. SelectionResult oder None wenn keine Auswahl möglich
  54. """
  55. ...
  56. def on_request_start(self, satellite: "Satellite") -> None:
  57. """
  58. Wird aufgerufen, wenn eine Anfrage an einen Satellite beginnt.
  59. Kann von Strategien überschrieben werden, die den Zustand
  60. verfolgen müssen (z.B. Least-Connections).
  61. Args:
  62. satellite: Der verwendete Satellite
  63. """
  64. pass
  65. def on_request_end(self, satellite: "Satellite", success: bool = True) -> None:
  66. """
  67. Wird aufgerufen, wenn eine Anfrage an einen Satellite endet.
  68. Args:
  69. satellite: Der verwendete Satellite
  70. success: Ob die Anfrage erfolgreich war
  71. """
  72. pass
  73. def reset(self) -> None:
  74. """Setzt den internen Zustand zurück."""
  75. pass
  76. class LoadBalancer:
  77. """
  78. Load-Balancer mit konfigurierbarer Strategie.
  79. Wählt Satellites basierend auf der konfigurierten Strategie aus.
  80. Unterstützt Fallback-Strategie und Filter.
  81. Example:
  82. balancer = LoadBalancer(RoundRobinStrategy())
  83. # Satellite auswählen
  84. result = balancer.select(satellites)
  85. if result:
  86. satellite = result.satellite
  87. await satellite.speak("Hallo")
  88. balancer.end_request(satellite)
  89. # Mit Filter
  90. result = balancer.select(
  91. satellites,
  92. filter_func=lambda s: s.room_id == "wohnzimmer"
  93. )
  94. """
  95. def __init__(
  96. self,
  97. strategy: LoadBalancingStrategy,
  98. fallback_strategy: LoadBalancingStrategy | None = None,
  99. ) -> None:
  100. """
  101. Initialisiert den Load-Balancer.
  102. Args:
  103. strategy: Primäre Strategie
  104. fallback_strategy: Optionale Fallback-Strategie
  105. """
  106. self._strategy = strategy
  107. self._fallback = fallback_strategy
  108. self._total_requests: int = 0
  109. self._failed_requests: int = 0
  110. @property
  111. def strategy(self) -> LoadBalancingStrategy:
  112. """Die aktive Strategie."""
  113. return self._strategy
  114. @strategy.setter
  115. def strategy(self, value: LoadBalancingStrategy) -> None:
  116. self._strategy = value
  117. @property
  118. def fallback_strategy(self) -> LoadBalancingStrategy | None:
  119. """Die Fallback-Strategie."""
  120. return self._fallback
  121. def select(
  122. self,
  123. satellites: list["Satellite"],
  124. filter_func: Any | None = None,
  125. context: dict[str, Any] | None = None,
  126. ) -> SelectionResult | None:
  127. """
  128. Wählt einen Satellite aus.
  129. Args:
  130. satellites: Liste verfügbarer Satellites
  131. filter_func: Optionale Filter-Funktion
  132. context: Optionaler Kontext
  133. Returns:
  134. SelectionResult oder None
  135. """
  136. # Nur verbundene Satellites
  137. available = [s for s in satellites if s.is_connected]
  138. # Optionalen Filter anwenden
  139. if filter_func:
  140. available = [s for s in available if filter_func(s)]
  141. if not available:
  142. return None
  143. self._total_requests += 1
  144. # Primäre Strategie versuchen
  145. result = self._strategy.select(available, context)
  146. # Fallback wenn nötig
  147. if result is None and self._fallback:
  148. result = self._fallback.select(available, context)
  149. if result:
  150. self._strategy.on_request_start(result.satellite)
  151. return result
  152. def start_request(self, satellite: "Satellite") -> None:
  153. """Markiert den Start einer Anfrage."""
  154. self._strategy.on_request_start(satellite)
  155. def end_request(self, satellite: "Satellite", success: bool = True) -> None:
  156. """Markiert das Ende einer Anfrage."""
  157. if not success:
  158. self._failed_requests += 1
  159. self._strategy.on_request_end(satellite, success)
  160. def reset(self) -> None:
  161. """Setzt den Zustand zurück."""
  162. self._strategy.reset()
  163. if self._fallback:
  164. self._fallback.reset()
  165. self._total_requests = 0
  166. self._failed_requests = 0
  167. def get_statistics(self) -> dict[str, Any]:
  168. """Gibt Statistiken zurück."""
  169. return {
  170. "strategy": self._strategy.name,
  171. "fallback_strategy": self._fallback.name if self._fallback else None,
  172. "total_requests": self._total_requests,
  173. "failed_requests": self._failed_requests,
  174. "success_rate": (
  175. (self._total_requests - self._failed_requests) / self._total_requests
  176. if self._total_requests > 0 else 1.0
  177. ),
  178. }