round_robin.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. # -*- coding: utf-8 -*-
  2. """
  3. Round-Robin Load-Balancing-Strategie.
  4. Verteilt Anfragen gleichmäßig auf alle verfügbaren Satellites.
  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 RoundRobinStrategy(LoadBalancingStrategy):
  14. """
  15. Round-Robin-Strategie.
  16. Wählt Satellites der Reihe nach aus und beginnt dann von vorne.
  17. Gewährleistet gleichmäßige Verteilung über alle Satellites.
  18. Example:
  19. strategy = RoundRobinStrategy()
  20. # Bei 3 Satellites: A, B, C
  21. # Auswahl-Reihenfolge: A, B, C, A, B, C, ...
  22. balancer = LoadBalancer(strategy)
  23. result = balancer.select(satellites)
  24. """
  25. def __init__(self) -> None:
  26. """Initialisiert die Round-Robin-Strategie."""
  27. self._current_index: int = 0
  28. self._satellite_order: list[str] = []
  29. @property
  30. def name(self) -> str:
  31. """Name der Strategie."""
  32. return "round_robin"
  33. def select(
  34. self,
  35. satellites: list["Satellite"],
  36. context: dict[str, Any] | None = None,
  37. ) -> SelectionResult | None:
  38. """
  39. Wählt den nächsten Satellite in der Rotation.
  40. Args:
  41. satellites: Liste verfügbarer Satellites
  42. context: Optionaler Kontext (wird ignoriert)
  43. Returns:
  44. SelectionResult oder None wenn keine Satellites verfügbar
  45. """
  46. if not satellites:
  47. return None
  48. # Sortiere Satellites für konsistente Reihenfolge
  49. sorted_satellites = sorted(satellites, key=lambda s: s.id)
  50. satellite_ids = [s.id for s in sorted_satellites]
  51. # Prüfe ob sich die Liste geändert hat
  52. if satellite_ids != self._satellite_order:
  53. self._satellite_order = satellite_ids
  54. # Index anpassen falls nötig
  55. if self._current_index >= len(sorted_satellites):
  56. self._current_index = 0
  57. # Satellite auswählen
  58. selected = sorted_satellites[self._current_index]
  59. # Index für nächste Auswahl vorbereiten
  60. self._current_index = (self._current_index + 1) % len(sorted_satellites)
  61. return SelectionResult(
  62. satellite=selected,
  63. reason=f"Round-Robin Index {self._current_index}",
  64. score=1.0 / len(sorted_satellites),
  65. )
  66. def reset(self) -> None:
  67. """Setzt den Index zurück."""
  68. self._current_index = 0
  69. self._satellite_order = []
  70. class WeightedRoundRobinStrategy(LoadBalancingStrategy):
  71. """
  72. Gewichtetes Round-Robin.
  73. Berücksichtigt Gewichte bei der Rotation. Satellites mit
  74. höherem Gewicht werden häufiger ausgewählt.
  75. Example:
  76. strategy = WeightedRoundRobinStrategy()
  77. strategy.set_weight("satellite_1", 3) # 3x so oft
  78. strategy.set_weight("satellite_2", 1) # Normal
  79. strategy.set_weight("satellite_3", 2) # 2x so oft
  80. # Reihenfolge: 1, 1, 1, 3, 3, 2, 1, 1, 1, ...
  81. """
  82. def __init__(self, default_weight: int = 1) -> None:
  83. """
  84. Initialisiert die gewichtete Round-Robin-Strategie.
  85. Args:
  86. default_weight: Standard-Gewicht für neue Satellites
  87. """
  88. self._default_weight = default_weight
  89. self._weights: dict[str, int] = {}
  90. self._current_index: int = 0
  91. self._expanded_list: list[str] = []
  92. @property
  93. def name(self) -> str:
  94. """Name der Strategie."""
  95. return "weighted_round_robin"
  96. def set_weight(self, satellite_id: str, weight: int) -> None:
  97. """
  98. Setzt das Gewicht für einen Satellite.
  99. Args:
  100. satellite_id: Satellite-ID
  101. weight: Gewicht (muss >= 1 sein)
  102. """
  103. self._weights[satellite_id] = max(1, weight)
  104. self._expanded_list = [] # Neuberechnung erzwingen
  105. def get_weight(self, satellite_id: str) -> int:
  106. """Gibt das Gewicht eines Satellites zurück."""
  107. return self._weights.get(satellite_id, self._default_weight)
  108. def _build_expanded_list(self, satellites: list["Satellite"]) -> list[str]:
  109. """Baut die expandierte Liste basierend auf Gewichten."""
  110. expanded = []
  111. for satellite in sorted(satellites, key=lambda s: s.id):
  112. weight = self.get_weight(satellite.id)
  113. expanded.extend([satellite.id] * weight)
  114. return expanded
  115. def select(
  116. self,
  117. satellites: list["Satellite"],
  118. context: dict[str, Any] | None = None,
  119. ) -> SelectionResult | None:
  120. """
  121. Wählt den nächsten Satellite basierend auf Gewichten.
  122. Args:
  123. satellites: Liste verfügbarer Satellites
  124. context: Optionaler Kontext
  125. Returns:
  126. SelectionResult oder None
  127. """
  128. if not satellites:
  129. return None
  130. # ID -> Satellite Mapping
  131. satellite_map = {s.id: s for s in satellites}
  132. # Expandierte Liste erstellen wenn nötig
  133. current_ids = sorted(s.id for s in satellites)
  134. if not self._expanded_list or set(self._expanded_list) != set(current_ids):
  135. self._expanded_list = self._build_expanded_list(satellites)
  136. if self._current_index >= len(self._expanded_list):
  137. self._current_index = 0
  138. if not self._expanded_list:
  139. return None
  140. # Auswahl
  141. selected_id = self._expanded_list[self._current_index]
  142. selected = satellite_map.get(selected_id)
  143. if selected is None:
  144. # Satellite nicht mehr verfügbar, Liste neu aufbauen
  145. self._expanded_list = self._build_expanded_list(satellites)
  146. self._current_index = 0
  147. if not self._expanded_list:
  148. return None
  149. selected_id = self._expanded_list[0]
  150. selected = satellite_map.get(selected_id)
  151. if selected is None:
  152. return None
  153. # Index vorrücken
  154. self._current_index = (self._current_index + 1) % len(self._expanded_list)
  155. return SelectionResult(
  156. satellite=selected,
  157. reason=f"Weighted Round-Robin (weight={self.get_weight(selected.id)})",
  158. score=self.get_weight(selected.id) / sum(self.get_weight(s.id) for s in satellites),
  159. )
  160. def reset(self) -> None:
  161. """Setzt den Zustand zurück."""
  162. self._current_index = 0
  163. self._expanded_list = []