# -*- coding: utf-8 -*- """ Gewichtete Load-Balancing-Strategie. Verteilt Anfragen basierend auf konfigurierbaren Gewichten. """ import random from dataclasses import dataclass, field from typing import Any, TYPE_CHECKING from trixy_core.satellite.loadbalancing.strategy import ( LoadBalancingStrategy, SelectionResult, ) if TYPE_CHECKING: from trixy_core.satellite.satellite import Satellite @dataclass class WeightConfig: """ Gewichtskonfiguration für einen Satellite. Attributes: weight: Basis-Gewicht (höher = mehr Anfragen) health_weight: Faktor basierend auf Gesundheit (0.0-1.0) capacity_weight: Faktor basierend auf verfügbarer Kapazität custom_factors: Zusätzliche benutzerdefinierte Faktoren """ weight: float = 1.0 health_weight: float = 1.0 capacity_weight: float = 1.0 custom_factors: dict[str, float] = field(default_factory=dict) @property def effective_weight(self) -> float: """Berechnet das effektive Gewicht.""" base = self.weight * self.health_weight * self.capacity_weight for factor in self.custom_factors.values(): base *= factor return max(0.0, base) class WeightedStrategy(LoadBalancingStrategy): """ Gewichtete Zufallsauswahl. Wählt Satellites zufällig basierend auf ihren Gewichten. Satellites mit höherem Gewicht werden häufiger gewählt. Example: strategy = WeightedStrategy() # Gewichte setzen strategy.set_weight("satellite_1", WeightConfig(weight=3.0)) strategy.set_weight("satellite_2", WeightConfig(weight=1.0)) strategy.set_weight("satellite_3", WeightConfig(weight=2.0)) # Auswahl (satellite_1 wird ~50% gewählt, satellite_3 ~33%, satellite_2 ~17%) result = strategy.select(satellites) """ def __init__(self, default_weight: float = 1.0) -> None: """ Initialisiert die gewichtete Strategie. Args: default_weight: Standard-Gewicht für neue Satellites """ self._default_weight = default_weight self._weights: dict[str, WeightConfig] = {} @property def name(self) -> str: """Name der Strategie.""" return "weighted" def set_weight(self, satellite_id: str, config: WeightConfig | float) -> None: """ Setzt das Gewicht für einen Satellite. Args: satellite_id: Satellite-ID config: WeightConfig oder einfaches Gewicht """ if isinstance(config, (int, float)): config = WeightConfig(weight=float(config)) self._weights[satellite_id] = config def get_weight(self, satellite_id: str) -> WeightConfig: """ Gibt die Gewichtskonfiguration zurück. Args: satellite_id: Satellite-ID Returns: WeightConfig """ return self._weights.get( satellite_id, WeightConfig(weight=self._default_weight) ) def update_health_weight(self, satellite_id: str, health: float) -> None: """ Aktualisiert das Gesundheitsgewicht. Args: satellite_id: Satellite-ID health: Gesundheitswert (0.0-1.0) """ config = self.get_weight(satellite_id) config.health_weight = max(0.0, min(1.0, health)) self._weights[satellite_id] = config def update_capacity_weight(self, satellite_id: str, capacity: float) -> None: """ Aktualisiert das Kapazitätsgewicht. Args: satellite_id: Satellite-ID capacity: Kapazitätswert (0.0-1.0) """ config = self.get_weight(satellite_id) config.capacity_weight = max(0.0, min(1.0, capacity)) self._weights[satellite_id] = config def select( self, satellites: list["Satellite"], context: dict[str, Any] | None = None, ) -> SelectionResult | None: """ Wählt einen Satellite basierend auf Gewichten. Args: satellites: Liste verfügbarer Satellites context: Optionaler Kontext Returns: SelectionResult oder None """ if not satellites: return None # Gewichte berechnen weights = [] for satellite in satellites: config = self.get_weight(satellite.id) weights.append(config.effective_weight) total_weight = sum(weights) if total_weight <= 0: # Fallback: Gleichverteilung selected = random.choice(satellites) return SelectionResult( satellite=selected, reason="Fallback (all weights zero)", score=1.0 / len(satellites), ) # Gewichtete Zufallsauswahl r = random.uniform(0, total_weight) cumulative = 0.0 for satellite, weight in zip(satellites, weights): cumulative += weight if r <= cumulative: return SelectionResult( satellite=satellite, reason=f"Weighted selection (weight={weight:.2f})", score=weight / total_weight, metadata={ "weight": weight, "total_weight": total_weight, "probability": weight / total_weight, } ) # Sollte nicht passieren, Fallback return SelectionResult( satellite=satellites[-1], reason="Fallback (rounding)", score=weights[-1] / total_weight if total_weight > 0 else 0, ) def reset(self) -> None: """Setzt die Gewichte auf Default zurück.""" self._weights.clear() def get_all_weights(self) -> dict[str, WeightConfig]: """Gibt alle Gewichte zurück.""" return self._weights.copy() class AdaptiveWeightedStrategy(WeightedStrategy): """ Adaptiv gewichtete Strategie. Passt Gewichte automatisch basierend auf Erfolg/Misserfolg an. Satellites mit höherer Erfolgsrate erhalten mehr Gewicht. Example: strategy = AdaptiveWeightedStrategy( success_boost=1.1, # +10% bei Erfolg failure_penalty=0.9 # -10% bei Misserfolg ) balancer = LoadBalancer(strategy) result = balancer.select(satellites) try: await result.satellite.speak("Test") balancer.end_request(result.satellite, success=True) except: balancer.end_request(result.satellite, success=False) """ def __init__( self, default_weight: float = 1.0, success_boost: float = 1.05, failure_penalty: float = 0.95, min_weight: float = 0.1, max_weight: float = 10.0, ) -> None: """ Initialisiert die adaptive Strategie. Args: default_weight: Start-Gewicht success_boost: Faktor bei Erfolg (>1.0) failure_penalty: Faktor bei Misserfolg (<1.0) min_weight: Minimales Gewicht max_weight: Maximales Gewicht """ super().__init__(default_weight) self._success_boost = success_boost self._failure_penalty = failure_penalty self._min_weight = min_weight self._max_weight = max_weight @property def name(self) -> str: """Name der Strategie.""" return "adaptive_weighted" def on_request_end(self, satellite: "Satellite", success: bool = True) -> None: """ Passt das Gewicht basierend auf Erfolg an. Args: satellite: Der Satellite success: Ob erfolgreich """ config = self.get_weight(satellite.id) if success: new_weight = config.weight * self._success_boost else: new_weight = config.weight * self._failure_penalty # Grenzen einhalten new_weight = max(self._min_weight, min(self._max_weight, new_weight)) config.weight = new_weight self._weights[satellite.id] = config class ResourceBasedStrategy(LoadBalancingStrategy): """ Ressourcenbasierte Auswahl. Wählt Satellites basierend auf verfügbaren Ressourcen (CPU, Speicher, Netzwerk-Bandbreite etc.). Example: strategy = ResourceBasedStrategy() # Ressourcen aktualisieren (z.B. aus Heartbeat) strategy.update_resources("satellite_1", { "cpu_available": 0.8, "memory_available": 0.6, "bandwidth_available": 0.9 }) result = strategy.select(satellites) """ def __init__( self, resource_weights: dict[str, float] | None = None, ) -> None: """ Initialisiert die ressourcenbasierte Strategie. Args: resource_weights: Gewichtung der Ressourcen """ self._resource_weights = resource_weights or { "cpu_available": 0.4, "memory_available": 0.3, "bandwidth_available": 0.3, } self._resources: dict[str, dict[str, float]] = {} @property def name(self) -> str: """Name der Strategie.""" return "resource_based" def update_resources(self, satellite_id: str, resources: dict[str, float]) -> None: """ Aktualisiert die Ressourcen-Information. Args: satellite_id: Satellite-ID resources: Dict mit Ressourcen (Werte 0.0-1.0) """ self._resources[satellite_id] = resources def get_resources(self, satellite_id: str) -> dict[str, float]: """Gibt die Ressourcen eines Satellites zurück.""" return self._resources.get(satellite_id, {}) def _calculate_score(self, satellite_id: str) -> float: """Berechnet den Ressourcen-Score (höher = mehr Ressourcen).""" resources = self.get_resources(satellite_id) if not resources: return 0.5 # Default score = 0.0 total_weight = 0.0 for resource, weight in self._resource_weights.items(): if resource in resources: score += resources[resource] * weight total_weight += weight if total_weight > 0: return score / total_weight return 0.5 def select( self, satellites: list["Satellite"], context: dict[str, Any] | None = None, ) -> SelectionResult | None: """ Wählt den Satellite mit den meisten verfügbaren Ressourcen. Args: satellites: Liste verfügbarer Satellites context: Optionaler Kontext Returns: SelectionResult oder None """ if not satellites: return None # Sortiere nach Score (absteigend) sorted_satellites = sorted( satellites, key=lambda s: (-self._calculate_score(s.id), s.id) ) selected = sorted_satellites[0] score = self._calculate_score(selected.id) return SelectionResult( satellite=selected, reason=f"Resource-based (score={score:.3f})", score=score, metadata={ "resources": self.get_resources(selected.id), "calculated_score": score, } ) def reset(self) -> None: """Setzt die Ressourcen-Daten zurück.""" self._resources.clear()