| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- # -*- coding: utf-8 -*-
- """
- Backup-Zuweisungen für Satellite-Failover.
- Verwaltet die Zuordnung von Backup-Satellites zu
- Primär-Satellites für Failover-Szenarien.
- """
- from dataclasses import dataclass, field
- from datetime import datetime
- from enum import IntEnum, auto
- from typing import Any, Callable, TYPE_CHECKING
- if TYPE_CHECKING:
- from trixy_core.satellite.satellite import Satellite
- class BackupStrategy(IntEnum):
- """Strategie für Backup-Auswahl."""
- EXPLICIT = auto() # Explizit zugewiesene Backups
- SAME_ROOM = auto() # Backup im selben Raum
- SAME_ZONE = auto() # Backup in derselben Zone
- ANY_AVAILABLE = auto() # Jeder verfügbare Satellite
- HEALTH_BASED = auto() # Basierend auf Health-Score
- @dataclass
- class BackupAssignment:
- """
- Zuweisung eines Backup-Satellites.
- Attributes:
- primary_id: ID des Primär-Satellites
- backup_id: ID des Backup-Satellites
- priority: Priorität (höher = bevorzugt)
- strategy: Verwendete Strategie
- created_at: Erstellungszeitpunkt
- last_used: Letzte Verwendung
- use_count: Anzahl Verwendungen
- """
- primary_id: str
- backup_id: str
- priority: int = 0
- strategy: BackupStrategy = BackupStrategy.EXPLICIT
- created_at: datetime = field(default_factory=datetime.now)
- last_used: datetime | None = None
- use_count: int = 0
- def record_use(self) -> None:
- """Zeichnet eine Verwendung auf."""
- self.last_used = datetime.now()
- self.use_count += 1
- def to_dict(self) -> dict[str, Any]:
- """Konvertiert zu Dictionary."""
- return {
- "primary_id": self.primary_id,
- "backup_id": self.backup_id,
- "priority": self.priority,
- "strategy": self.strategy.name,
- "created_at": self.created_at.isoformat(),
- "last_used": self.last_used.isoformat() if self.last_used else None,
- "use_count": self.use_count,
- }
- class BackupRegistry:
- """
- Registry für Backup-Zuweisungen.
- Verwaltet die Zuordnung von Backup-Satellites und
- bietet verschiedene Strategien zur Backup-Auswahl.
- Example:
- registry = BackupRegistry()
- # Explizite Zuweisung
- registry.assign(
- primary_id="satellite_1",
- backup_id="satellite_2",
- priority=10
- )
- # Alternative Backups
- registry.assign("satellite_1", "satellite_3", priority=5)
- # Backup abrufen
- backup = registry.get_best_backup("satellite_1")
- if backup:
- print(f"Backup: {backup.backup_id}")
- # Dynamische Backups aktivieren
- registry.set_strategy(BackupStrategy.SAME_ROOM)
- """
- def __init__(
- self,
- default_strategy: BackupStrategy = BackupStrategy.EXPLICIT,
- get_satellite: Callable[[str], "Satellite | None"] | None = None,
- get_all_satellites: Callable[[], list["Satellite"]] | None = None,
- ) -> None:
- """
- Initialisiert die Backup-Registry.
- Args:
- default_strategy: Standard-Strategie
- get_satellite: Funktion zum Abrufen eines Satellites
- get_all_satellites: Funktion zum Abrufen aller Satellites
- """
- self._strategy = default_strategy
- self._get_satellite = get_satellite
- self._get_all_satellites = get_all_satellites
- self._assignments: dict[str, list[BackupAssignment]] = {}
- self._health_scores: dict[str, float] = {}
- @property
- def strategy(self) -> BackupStrategy:
- """Aktive Strategie."""
- return self._strategy
- def set_strategy(self, strategy: BackupStrategy) -> None:
- """Setzt die Strategie."""
- self._strategy = strategy
- def assign(
- self,
- primary_id: str,
- backup_id: str,
- priority: int = 0,
- strategy: BackupStrategy = BackupStrategy.EXPLICIT,
- ) -> BackupAssignment:
- """
- Weist einen Backup-Satellite zu.
- Args:
- primary_id: ID des Primär-Satellites
- backup_id: ID des Backup-Satellites
- priority: Priorität (höher = bevorzugt)
- strategy: Verwendete Strategie
- Returns:
- Die erstellte Zuweisung
- """
- if primary_id == backup_id:
- raise ValueError("Primary und Backup können nicht identisch sein")
- assignment = BackupAssignment(
- primary_id=primary_id,
- backup_id=backup_id,
- priority=priority,
- strategy=strategy,
- )
- if primary_id not in self._assignments:
- self._assignments[primary_id] = []
- # Prüfe ob bereits vorhanden
- existing = [a for a in self._assignments[primary_id] if a.backup_id == backup_id]
- if existing:
- # Aktualisiere existierende Zuweisung
- existing[0].priority = priority
- existing[0].strategy = strategy
- return existing[0]
- self._assignments[primary_id].append(assignment)
- # Nach Priorität sortieren
- self._assignments[primary_id].sort(key=lambda a: -a.priority)
- return assignment
- def unassign(self, primary_id: str, backup_id: str | None = None) -> int:
- """
- Entfernt Backup-Zuweisungen.
- Args:
- primary_id: ID des Primär-Satellites
- backup_id: Optionale spezifische Backup-ID (None = alle)
- Returns:
- Anzahl entfernter Zuweisungen
- """
- if primary_id not in self._assignments:
- return 0
- if backup_id is None:
- count = len(self._assignments[primary_id])
- del self._assignments[primary_id]
- return count
- before = len(self._assignments[primary_id])
- self._assignments[primary_id] = [
- a for a in self._assignments[primary_id]
- if a.backup_id != backup_id
- ]
- return before - len(self._assignments[primary_id])
- def get_backups(self, primary_id: str) -> list[BackupAssignment]:
- """
- Gibt alle Backups für einen Primary zurück.
- Args:
- primary_id: ID des Primär-Satellites
- Returns:
- Liste der Backup-Zuweisungen (nach Priorität sortiert)
- """
- return self._assignments.get(primary_id, []).copy()
- def get_best_backup(
- self,
- primary_id: str,
- exclude: set[str] | None = None,
- ) -> BackupAssignment | None:
- """
- Gibt das beste verfügbare Backup zurück.
- Berücksichtigt:
- - Explizite Zuweisungen
- - Aktive Strategie
- - Verfügbarkeit
- - Health-Score
- Args:
- primary_id: ID des Primär-Satellites
- exclude: IDs die ausgeschlossen werden sollen
- Returns:
- Beste Backup-Zuweisung oder None
- """
- exclude = exclude or set()
- exclude.add(primary_id)
- # Explizite Zuweisungen zuerst
- for assignment in self._assignments.get(primary_id, []):
- if assignment.backup_id in exclude:
- continue
- if self._is_backup_available(assignment.backup_id):
- return assignment
- # Fallback auf Strategie
- return self._find_dynamic_backup(primary_id, exclude)
- def _is_backup_available(self, backup_id: str) -> bool:
- """Prüft, ob ein Backup verfügbar ist."""
- if self._get_satellite is None:
- return True # Annahme: verfügbar wenn nicht prüfbar
- backup = self._get_satellite(backup_id)
- return backup is not None and backup.is_connected
- def _find_dynamic_backup(
- self,
- primary_id: str,
- exclude: set[str],
- ) -> BackupAssignment | None:
- """Findet ein Backup basierend auf der aktiven Strategie."""
- if self._get_satellite is None or self._get_all_satellites is None:
- return None
- primary = self._get_satellite(primary_id)
- candidates = self._get_all_satellites()
- # Filtern: verbunden und nicht ausgeschlossen
- candidates = [
- s for s in candidates
- if s.is_connected and s.id not in exclude
- ]
- if not candidates:
- return None
- # Strategie anwenden
- if self._strategy == BackupStrategy.SAME_ROOM:
- if primary:
- candidates = [s for s in candidates if s.room_id == primary.room_id]
- elif self._strategy == BackupStrategy.HEALTH_BASED:
- # Nach Health-Score sortieren
- candidates = sorted(
- candidates,
- key=lambda s: self._health_scores.get(s.id, 50.0),
- reverse=True
- )
- if not candidates:
- return None
- # Bestes Kandidat auswählen
- backup = candidates[0]
- return BackupAssignment(
- primary_id=primary_id,
- backup_id=backup.id,
- priority=0,
- strategy=self._strategy,
- )
- def update_health_score(self, satellite_id: str, score: float) -> None:
- """
- Aktualisiert den Health-Score für Strategy-basierte Auswahl.
- Args:
- satellite_id: Satellite-ID
- score: Health-Score (0-100)
- """
- self._health_scores[satellite_id] = score
- def get_all_assignments(self) -> dict[str, list[BackupAssignment]]:
- """Gibt alle Zuweisungen zurück."""
- return {
- primary: [a for a in assignments]
- for primary, assignments in self._assignments.items()
- }
- def get_statistics(self) -> dict[str, Any]:
- """Gibt Statistiken zurück."""
- total_assignments = sum(
- len(assignments)
- for assignments in self._assignments.values()
- )
- primaries_with_backup = len(self._assignments)
- total_uses = sum(
- a.use_count
- for assignments in self._assignments.values()
- for a in assignments
- )
- return {
- "total_assignments": total_assignments,
- "primaries_with_backup": primaries_with_backup,
- "total_failover_uses": total_uses,
- "strategy": self._strategy.name,
- }
- def to_dict(self) -> dict[str, Any]:
- """Serialisiert die Registry."""
- return {
- "strategy": self._strategy.name,
- "assignments": {
- primary: [a.to_dict() for a in assignments]
- for primary, assignments in self._assignments.items()
- },
- }
- @classmethod
- def from_dict(
- cls,
- data: dict[str, Any],
- get_satellite: Callable[[str], "Satellite | None"] | None = None,
- get_all_satellites: Callable[[], list["Satellite"]] | None = None,
- ) -> "BackupRegistry":
- """Erstellt eine Registry aus einem Dictionary."""
- strategy = BackupStrategy[data.get("strategy", "EXPLICIT")]
- registry = cls(
- default_strategy=strategy,
- get_satellite=get_satellite,
- get_all_satellites=get_all_satellites,
- )
- for primary_id, assignments in data.get("assignments", {}).items():
- for a_data in assignments:
- registry.assign(
- primary_id=a_data["primary_id"],
- backup_id=a_data["backup_id"],
- priority=a_data.get("priority", 0),
- strategy=BackupStrategy[a_data.get("strategy", "EXPLICIT")],
- )
- return registry
|