| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- # -*- 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()
|