backup.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. # -*- coding: utf-8 -*-
  2. """
  3. Backup-Zuweisungen für Satellite-Failover.
  4. Verwaltet die Zuordnung von Backup-Satellites zu
  5. Primär-Satellites für Failover-Szenarien.
  6. """
  7. from dataclasses import dataclass, field
  8. from datetime import datetime
  9. from enum import IntEnum, auto
  10. from typing import Any, Callable, TYPE_CHECKING
  11. if TYPE_CHECKING:
  12. from trixy_core.satellite.satellite import Satellite
  13. class BackupStrategy(IntEnum):
  14. """Strategie für Backup-Auswahl."""
  15. EXPLICIT = auto() # Explizit zugewiesene Backups
  16. SAME_ROOM = auto() # Backup im selben Raum
  17. SAME_ZONE = auto() # Backup in derselben Zone
  18. ANY_AVAILABLE = auto() # Jeder verfügbare Satellite
  19. HEALTH_BASED = auto() # Basierend auf Health-Score
  20. @dataclass
  21. class BackupAssignment:
  22. """
  23. Zuweisung eines Backup-Satellites.
  24. Attributes:
  25. primary_id: ID des Primär-Satellites
  26. backup_id: ID des Backup-Satellites
  27. priority: Priorität (höher = bevorzugt)
  28. strategy: Verwendete Strategie
  29. created_at: Erstellungszeitpunkt
  30. last_used: Letzte Verwendung
  31. use_count: Anzahl Verwendungen
  32. """
  33. primary_id: str
  34. backup_id: str
  35. priority: int = 0
  36. strategy: BackupStrategy = BackupStrategy.EXPLICIT
  37. created_at: datetime = field(default_factory=datetime.now)
  38. last_used: datetime | None = None
  39. use_count: int = 0
  40. def record_use(self) -> None:
  41. """Zeichnet eine Verwendung auf."""
  42. self.last_used = datetime.now()
  43. self.use_count += 1
  44. def to_dict(self) -> dict[str, Any]:
  45. """Konvertiert zu Dictionary."""
  46. return {
  47. "primary_id": self.primary_id,
  48. "backup_id": self.backup_id,
  49. "priority": self.priority,
  50. "strategy": self.strategy.name,
  51. "created_at": self.created_at.isoformat(),
  52. "last_used": self.last_used.isoformat() if self.last_used else None,
  53. "use_count": self.use_count,
  54. }
  55. class BackupRegistry:
  56. """
  57. Registry für Backup-Zuweisungen.
  58. Verwaltet die Zuordnung von Backup-Satellites und
  59. bietet verschiedene Strategien zur Backup-Auswahl.
  60. Example:
  61. registry = BackupRegistry()
  62. # Explizite Zuweisung
  63. registry.assign(
  64. primary_id="satellite_1",
  65. backup_id="satellite_2",
  66. priority=10
  67. )
  68. # Alternative Backups
  69. registry.assign("satellite_1", "satellite_3", priority=5)
  70. # Backup abrufen
  71. backup = registry.get_best_backup("satellite_1")
  72. if backup:
  73. print(f"Backup: {backup.backup_id}")
  74. # Dynamische Backups aktivieren
  75. registry.set_strategy(BackupStrategy.SAME_ROOM)
  76. """
  77. def __init__(
  78. self,
  79. default_strategy: BackupStrategy = BackupStrategy.EXPLICIT,
  80. get_satellite: Callable[[str], "Satellite | None"] | None = None,
  81. get_all_satellites: Callable[[], list["Satellite"]] | None = None,
  82. ) -> None:
  83. """
  84. Initialisiert die Backup-Registry.
  85. Args:
  86. default_strategy: Standard-Strategie
  87. get_satellite: Funktion zum Abrufen eines Satellites
  88. get_all_satellites: Funktion zum Abrufen aller Satellites
  89. """
  90. self._strategy = default_strategy
  91. self._get_satellite = get_satellite
  92. self._get_all_satellites = get_all_satellites
  93. self._assignments: dict[str, list[BackupAssignment]] = {}
  94. self._health_scores: dict[str, float] = {}
  95. @property
  96. def strategy(self) -> BackupStrategy:
  97. """Aktive Strategie."""
  98. return self._strategy
  99. def set_strategy(self, strategy: BackupStrategy) -> None:
  100. """Setzt die Strategie."""
  101. self._strategy = strategy
  102. def assign(
  103. self,
  104. primary_id: str,
  105. backup_id: str,
  106. priority: int = 0,
  107. strategy: BackupStrategy = BackupStrategy.EXPLICIT,
  108. ) -> BackupAssignment:
  109. """
  110. Weist einen Backup-Satellite zu.
  111. Args:
  112. primary_id: ID des Primär-Satellites
  113. backup_id: ID des Backup-Satellites
  114. priority: Priorität (höher = bevorzugt)
  115. strategy: Verwendete Strategie
  116. Returns:
  117. Die erstellte Zuweisung
  118. """
  119. if primary_id == backup_id:
  120. raise ValueError("Primary und Backup können nicht identisch sein")
  121. assignment = BackupAssignment(
  122. primary_id=primary_id,
  123. backup_id=backup_id,
  124. priority=priority,
  125. strategy=strategy,
  126. )
  127. if primary_id not in self._assignments:
  128. self._assignments[primary_id] = []
  129. # Prüfe ob bereits vorhanden
  130. existing = [a for a in self._assignments[primary_id] if a.backup_id == backup_id]
  131. if existing:
  132. # Aktualisiere existierende Zuweisung
  133. existing[0].priority = priority
  134. existing[0].strategy = strategy
  135. return existing[0]
  136. self._assignments[primary_id].append(assignment)
  137. # Nach Priorität sortieren
  138. self._assignments[primary_id].sort(key=lambda a: -a.priority)
  139. return assignment
  140. def unassign(self, primary_id: str, backup_id: str | None = None) -> int:
  141. """
  142. Entfernt Backup-Zuweisungen.
  143. Args:
  144. primary_id: ID des Primär-Satellites
  145. backup_id: Optionale spezifische Backup-ID (None = alle)
  146. Returns:
  147. Anzahl entfernter Zuweisungen
  148. """
  149. if primary_id not in self._assignments:
  150. return 0
  151. if backup_id is None:
  152. count = len(self._assignments[primary_id])
  153. del self._assignments[primary_id]
  154. return count
  155. before = len(self._assignments[primary_id])
  156. self._assignments[primary_id] = [
  157. a for a in self._assignments[primary_id]
  158. if a.backup_id != backup_id
  159. ]
  160. return before - len(self._assignments[primary_id])
  161. def get_backups(self, primary_id: str) -> list[BackupAssignment]:
  162. """
  163. Gibt alle Backups für einen Primary zurück.
  164. Args:
  165. primary_id: ID des Primär-Satellites
  166. Returns:
  167. Liste der Backup-Zuweisungen (nach Priorität sortiert)
  168. """
  169. return self._assignments.get(primary_id, []).copy()
  170. def get_best_backup(
  171. self,
  172. primary_id: str,
  173. exclude: set[str] | None = None,
  174. ) -> BackupAssignment | None:
  175. """
  176. Gibt das beste verfügbare Backup zurück.
  177. Berücksichtigt:
  178. - Explizite Zuweisungen
  179. - Aktive Strategie
  180. - Verfügbarkeit
  181. - Health-Score
  182. Args:
  183. primary_id: ID des Primär-Satellites
  184. exclude: IDs die ausgeschlossen werden sollen
  185. Returns:
  186. Beste Backup-Zuweisung oder None
  187. """
  188. exclude = exclude or set()
  189. exclude.add(primary_id)
  190. # Explizite Zuweisungen zuerst
  191. for assignment in self._assignments.get(primary_id, []):
  192. if assignment.backup_id in exclude:
  193. continue
  194. if self._is_backup_available(assignment.backup_id):
  195. return assignment
  196. # Fallback auf Strategie
  197. return self._find_dynamic_backup(primary_id, exclude)
  198. def _is_backup_available(self, backup_id: str) -> bool:
  199. """Prüft, ob ein Backup verfügbar ist."""
  200. if self._get_satellite is None:
  201. return True # Annahme: verfügbar wenn nicht prüfbar
  202. backup = self._get_satellite(backup_id)
  203. return backup is not None and backup.is_connected
  204. def _find_dynamic_backup(
  205. self,
  206. primary_id: str,
  207. exclude: set[str],
  208. ) -> BackupAssignment | None:
  209. """Findet ein Backup basierend auf der aktiven Strategie."""
  210. if self._get_satellite is None or self._get_all_satellites is None:
  211. return None
  212. primary = self._get_satellite(primary_id)
  213. candidates = self._get_all_satellites()
  214. # Filtern: verbunden und nicht ausgeschlossen
  215. candidates = [
  216. s for s in candidates
  217. if s.is_connected and s.id not in exclude
  218. ]
  219. if not candidates:
  220. return None
  221. # Strategie anwenden
  222. if self._strategy == BackupStrategy.SAME_ROOM:
  223. if primary:
  224. candidates = [s for s in candidates if s.room_id == primary.room_id]
  225. elif self._strategy == BackupStrategy.HEALTH_BASED:
  226. # Nach Health-Score sortieren
  227. candidates = sorted(
  228. candidates,
  229. key=lambda s: self._health_scores.get(s.id, 50.0),
  230. reverse=True
  231. )
  232. if not candidates:
  233. return None
  234. # Bestes Kandidat auswählen
  235. backup = candidates[0]
  236. return BackupAssignment(
  237. primary_id=primary_id,
  238. backup_id=backup.id,
  239. priority=0,
  240. strategy=self._strategy,
  241. )
  242. def update_health_score(self, satellite_id: str, score: float) -> None:
  243. """
  244. Aktualisiert den Health-Score für Strategy-basierte Auswahl.
  245. Args:
  246. satellite_id: Satellite-ID
  247. score: Health-Score (0-100)
  248. """
  249. self._health_scores[satellite_id] = score
  250. def get_all_assignments(self) -> dict[str, list[BackupAssignment]]:
  251. """Gibt alle Zuweisungen zurück."""
  252. return {
  253. primary: [a for a in assignments]
  254. for primary, assignments in self._assignments.items()
  255. }
  256. def get_statistics(self) -> dict[str, Any]:
  257. """Gibt Statistiken zurück."""
  258. total_assignments = sum(
  259. len(assignments)
  260. for assignments in self._assignments.values()
  261. )
  262. primaries_with_backup = len(self._assignments)
  263. total_uses = sum(
  264. a.use_count
  265. for assignments in self._assignments.values()
  266. for a in assignments
  267. )
  268. return {
  269. "total_assignments": total_assignments,
  270. "primaries_with_backup": primaries_with_backup,
  271. "total_failover_uses": total_uses,
  272. "strategy": self._strategy.name,
  273. }
  274. def to_dict(self) -> dict[str, Any]:
  275. """Serialisiert die Registry."""
  276. return {
  277. "strategy": self._strategy.name,
  278. "assignments": {
  279. primary: [a.to_dict() for a in assignments]
  280. for primary, assignments in self._assignments.items()
  281. },
  282. }
  283. @classmethod
  284. def from_dict(
  285. cls,
  286. data: dict[str, Any],
  287. get_satellite: Callable[[str], "Satellite | None"] | None = None,
  288. get_all_satellites: Callable[[], list["Satellite"]] | None = None,
  289. ) -> "BackupRegistry":
  290. """Erstellt eine Registry aus einem Dictionary."""
  291. strategy = BackupStrategy[data.get("strategy", "EXPLICIT")]
  292. registry = cls(
  293. default_strategy=strategy,
  294. get_satellite=get_satellite,
  295. get_all_satellites=get_all_satellites,
  296. )
  297. for primary_id, assignments in data.get("assignments", {}).items():
  298. for a_data in assignments:
  299. registry.assign(
  300. primary_id=a_data["primary_id"],
  301. backup_id=a_data["backup_id"],
  302. priority=a_data.get("priority", 0),
  303. strategy=BackupStrategy[a_data.get("strategy", "EXPLICIT")],
  304. )
  305. return registry