weighted.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. # -*- coding: utf-8 -*-
  2. """
  3. Gewichtete Load-Balancing-Strategie.
  4. Verteilt Anfragen basierend auf konfigurierbaren Gewichten.
  5. """
  6. import random
  7. from dataclasses import dataclass, field
  8. from typing import Any, TYPE_CHECKING
  9. from trixy_core.satellite.loadbalancing.strategy import (
  10. LoadBalancingStrategy,
  11. SelectionResult,
  12. )
  13. if TYPE_CHECKING:
  14. from trixy_core.satellite.satellite import Satellite
  15. @dataclass
  16. class WeightConfig:
  17. """
  18. Gewichtskonfiguration für einen Satellite.
  19. Attributes:
  20. weight: Basis-Gewicht (höher = mehr Anfragen)
  21. health_weight: Faktor basierend auf Gesundheit (0.0-1.0)
  22. capacity_weight: Faktor basierend auf verfügbarer Kapazität
  23. custom_factors: Zusätzliche benutzerdefinierte Faktoren
  24. """
  25. weight: float = 1.0
  26. health_weight: float = 1.0
  27. capacity_weight: float = 1.0
  28. custom_factors: dict[str, float] = field(default_factory=dict)
  29. @property
  30. def effective_weight(self) -> float:
  31. """Berechnet das effektive Gewicht."""
  32. base = self.weight * self.health_weight * self.capacity_weight
  33. for factor in self.custom_factors.values():
  34. base *= factor
  35. return max(0.0, base)
  36. class WeightedStrategy(LoadBalancingStrategy):
  37. """
  38. Gewichtete Zufallsauswahl.
  39. Wählt Satellites zufällig basierend auf ihren Gewichten.
  40. Satellites mit höherem Gewicht werden häufiger gewählt.
  41. Example:
  42. strategy = WeightedStrategy()
  43. # Gewichte setzen
  44. strategy.set_weight("satellite_1", WeightConfig(weight=3.0))
  45. strategy.set_weight("satellite_2", WeightConfig(weight=1.0))
  46. strategy.set_weight("satellite_3", WeightConfig(weight=2.0))
  47. # Auswahl (satellite_1 wird ~50% gewählt, satellite_3 ~33%, satellite_2 ~17%)
  48. result = strategy.select(satellites)
  49. """
  50. def __init__(self, default_weight: float = 1.0) -> None:
  51. """
  52. Initialisiert die gewichtete Strategie.
  53. Args:
  54. default_weight: Standard-Gewicht für neue Satellites
  55. """
  56. self._default_weight = default_weight
  57. self._weights: dict[str, WeightConfig] = {}
  58. @property
  59. def name(self) -> str:
  60. """Name der Strategie."""
  61. return "weighted"
  62. def set_weight(self, satellite_id: str, config: WeightConfig | float) -> None:
  63. """
  64. Setzt das Gewicht für einen Satellite.
  65. Args:
  66. satellite_id: Satellite-ID
  67. config: WeightConfig oder einfaches Gewicht
  68. """
  69. if isinstance(config, (int, float)):
  70. config = WeightConfig(weight=float(config))
  71. self._weights[satellite_id] = config
  72. def get_weight(self, satellite_id: str) -> WeightConfig:
  73. """
  74. Gibt die Gewichtskonfiguration zurück.
  75. Args:
  76. satellite_id: Satellite-ID
  77. Returns:
  78. WeightConfig
  79. """
  80. return self._weights.get(
  81. satellite_id,
  82. WeightConfig(weight=self._default_weight)
  83. )
  84. def update_health_weight(self, satellite_id: str, health: float) -> None:
  85. """
  86. Aktualisiert das Gesundheitsgewicht.
  87. Args:
  88. satellite_id: Satellite-ID
  89. health: Gesundheitswert (0.0-1.0)
  90. """
  91. config = self.get_weight(satellite_id)
  92. config.health_weight = max(0.0, min(1.0, health))
  93. self._weights[satellite_id] = config
  94. def update_capacity_weight(self, satellite_id: str, capacity: float) -> None:
  95. """
  96. Aktualisiert das Kapazitätsgewicht.
  97. Args:
  98. satellite_id: Satellite-ID
  99. capacity: Kapazitätswert (0.0-1.0)
  100. """
  101. config = self.get_weight(satellite_id)
  102. config.capacity_weight = max(0.0, min(1.0, capacity))
  103. self._weights[satellite_id] = config
  104. def select(
  105. self,
  106. satellites: list["Satellite"],
  107. context: dict[str, Any] | None = None,
  108. ) -> SelectionResult | None:
  109. """
  110. Wählt einen Satellite basierend auf Gewichten.
  111. Args:
  112. satellites: Liste verfügbarer Satellites
  113. context: Optionaler Kontext
  114. Returns:
  115. SelectionResult oder None
  116. """
  117. if not satellites:
  118. return None
  119. # Gewichte berechnen
  120. weights = []
  121. for satellite in satellites:
  122. config = self.get_weight(satellite.id)
  123. weights.append(config.effective_weight)
  124. total_weight = sum(weights)
  125. if total_weight <= 0:
  126. # Fallback: Gleichverteilung
  127. selected = random.choice(satellites)
  128. return SelectionResult(
  129. satellite=selected,
  130. reason="Fallback (all weights zero)",
  131. score=1.0 / len(satellites),
  132. )
  133. # Gewichtete Zufallsauswahl
  134. r = random.uniform(0, total_weight)
  135. cumulative = 0.0
  136. for satellite, weight in zip(satellites, weights):
  137. cumulative += weight
  138. if r <= cumulative:
  139. return SelectionResult(
  140. satellite=satellite,
  141. reason=f"Weighted selection (weight={weight:.2f})",
  142. score=weight / total_weight,
  143. metadata={
  144. "weight": weight,
  145. "total_weight": total_weight,
  146. "probability": weight / total_weight,
  147. }
  148. )
  149. # Sollte nicht passieren, Fallback
  150. return SelectionResult(
  151. satellite=satellites[-1],
  152. reason="Fallback (rounding)",
  153. score=weights[-1] / total_weight if total_weight > 0 else 0,
  154. )
  155. def reset(self) -> None:
  156. """Setzt die Gewichte auf Default zurück."""
  157. self._weights.clear()
  158. def get_all_weights(self) -> dict[str, WeightConfig]:
  159. """Gibt alle Gewichte zurück."""
  160. return self._weights.copy()
  161. class AdaptiveWeightedStrategy(WeightedStrategy):
  162. """
  163. Adaptiv gewichtete Strategie.
  164. Passt Gewichte automatisch basierend auf Erfolg/Misserfolg an.
  165. Satellites mit höherer Erfolgsrate erhalten mehr Gewicht.
  166. Example:
  167. strategy = AdaptiveWeightedStrategy(
  168. success_boost=1.1, # +10% bei Erfolg
  169. failure_penalty=0.9 # -10% bei Misserfolg
  170. )
  171. balancer = LoadBalancer(strategy)
  172. result = balancer.select(satellites)
  173. try:
  174. await result.satellite.speak("Test")
  175. balancer.end_request(result.satellite, success=True)
  176. except:
  177. balancer.end_request(result.satellite, success=False)
  178. """
  179. def __init__(
  180. self,
  181. default_weight: float = 1.0,
  182. success_boost: float = 1.05,
  183. failure_penalty: float = 0.95,
  184. min_weight: float = 0.1,
  185. max_weight: float = 10.0,
  186. ) -> None:
  187. """
  188. Initialisiert die adaptive Strategie.
  189. Args:
  190. default_weight: Start-Gewicht
  191. success_boost: Faktor bei Erfolg (>1.0)
  192. failure_penalty: Faktor bei Misserfolg (<1.0)
  193. min_weight: Minimales Gewicht
  194. max_weight: Maximales Gewicht
  195. """
  196. super().__init__(default_weight)
  197. self._success_boost = success_boost
  198. self._failure_penalty = failure_penalty
  199. self._min_weight = min_weight
  200. self._max_weight = max_weight
  201. @property
  202. def name(self) -> str:
  203. """Name der Strategie."""
  204. return "adaptive_weighted"
  205. def on_request_end(self, satellite: "Satellite", success: bool = True) -> None:
  206. """
  207. Passt das Gewicht basierend auf Erfolg an.
  208. Args:
  209. satellite: Der Satellite
  210. success: Ob erfolgreich
  211. """
  212. config = self.get_weight(satellite.id)
  213. if success:
  214. new_weight = config.weight * self._success_boost
  215. else:
  216. new_weight = config.weight * self._failure_penalty
  217. # Grenzen einhalten
  218. new_weight = max(self._min_weight, min(self._max_weight, new_weight))
  219. config.weight = new_weight
  220. self._weights[satellite.id] = config
  221. class ResourceBasedStrategy(LoadBalancingStrategy):
  222. """
  223. Ressourcenbasierte Auswahl.
  224. Wählt Satellites basierend auf verfügbaren Ressourcen
  225. (CPU, Speicher, Netzwerk-Bandbreite etc.).
  226. Example:
  227. strategy = ResourceBasedStrategy()
  228. # Ressourcen aktualisieren (z.B. aus Heartbeat)
  229. strategy.update_resources("satellite_1", {
  230. "cpu_available": 0.8,
  231. "memory_available": 0.6,
  232. "bandwidth_available": 0.9
  233. })
  234. result = strategy.select(satellites)
  235. """
  236. def __init__(
  237. self,
  238. resource_weights: dict[str, float] | None = None,
  239. ) -> None:
  240. """
  241. Initialisiert die ressourcenbasierte Strategie.
  242. Args:
  243. resource_weights: Gewichtung der Ressourcen
  244. """
  245. self._resource_weights = resource_weights or {
  246. "cpu_available": 0.4,
  247. "memory_available": 0.3,
  248. "bandwidth_available": 0.3,
  249. }
  250. self._resources: dict[str, dict[str, float]] = {}
  251. @property
  252. def name(self) -> str:
  253. """Name der Strategie."""
  254. return "resource_based"
  255. def update_resources(self, satellite_id: str, resources: dict[str, float]) -> None:
  256. """
  257. Aktualisiert die Ressourcen-Information.
  258. Args:
  259. satellite_id: Satellite-ID
  260. resources: Dict mit Ressourcen (Werte 0.0-1.0)
  261. """
  262. self._resources[satellite_id] = resources
  263. def get_resources(self, satellite_id: str) -> dict[str, float]:
  264. """Gibt die Ressourcen eines Satellites zurück."""
  265. return self._resources.get(satellite_id, {})
  266. def _calculate_score(self, satellite_id: str) -> float:
  267. """Berechnet den Ressourcen-Score (höher = mehr Ressourcen)."""
  268. resources = self.get_resources(satellite_id)
  269. if not resources:
  270. return 0.5 # Default
  271. score = 0.0
  272. total_weight = 0.0
  273. for resource, weight in self._resource_weights.items():
  274. if resource in resources:
  275. score += resources[resource] * weight
  276. total_weight += weight
  277. if total_weight > 0:
  278. return score / total_weight
  279. return 0.5
  280. def select(
  281. self,
  282. satellites: list["Satellite"],
  283. context: dict[str, Any] | None = None,
  284. ) -> SelectionResult | None:
  285. """
  286. Wählt den Satellite mit den meisten verfügbaren Ressourcen.
  287. Args:
  288. satellites: Liste verfügbarer Satellites
  289. context: Optionaler Kontext
  290. Returns:
  291. SelectionResult oder None
  292. """
  293. if not satellites:
  294. return None
  295. # Sortiere nach Score (absteigend)
  296. sorted_satellites = sorted(
  297. satellites,
  298. key=lambda s: (-self._calculate_score(s.id), s.id)
  299. )
  300. selected = sorted_satellites[0]
  301. score = self._calculate_score(selected.id)
  302. return SelectionResult(
  303. satellite=selected,
  304. reason=f"Resource-based (score={score:.3f})",
  305. score=score,
  306. metadata={
  307. "resources": self.get_resources(selected.id),
  308. "calculated_score": score,
  309. }
  310. )
  311. def reset(self) -> None:
  312. """Setzt die Ressourcen-Daten zurück."""
  313. self._resources.clear()