cc119d3751
- ProjectStateStore (§6.4, §24.2): monotone State-/Projekt-Revisionen, Snapshot nach Verbindung, Delta mit neu/geaendert/geloescht, Szenenabruf als Zielzustand (Uebergang macht der Renderer, §18.1), Projekt-/Livezustand strikt getrennt - GroupRouter (§6.3, §10.1): Ziele All/Node/Output/ServerGroup, Regeln selected/tag_query/all, Commit-Vorschau (§17.5) - ClockEstimator (§6.4): RTT-Min-Filter (<=2x Min gegen Jitter), Offset-/Drift-Schaetzung (Drift erst ab 1 s Fenster belastbar), Showzeit -> lokale Node-Zeit - ActivationCoordinator (§6.5): zeitgestempelte Preset-Aktivierung, 200 ms Vorlauf, Arm/Execute/Ack-Kette, FAILED bei fehlender Arm-Bestaetigung (kein stilles Ueberbruecken) - 35 neue Unit-Tests; Gesamtsuite 293 gruen, Ruff gruen
117 lines
4.0 KiB
Python
117 lines
4.0 KiB
Python
"""Servergruppen und Zielrouting (PLAN.md §6.3 Bedienmodelle, §10.1 ServerGroup).
|
|
|
|
- Coordinator routet Commands an `All`, eine `ServerGroup`, einen einzelnen
|
|
`Node` oder einen `Output` (§6.3 Control-Center-Modell)
|
|
- Zielregel je Gruppe: all | selected | tag_query | feste Node-Liste
|
|
- Zielauflösung ist deterministisch und testbar; Gruppenänderungen zeigen
|
|
vor dem Commit, welche Nodes sie erhalten (§17.5)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import StrEnum
|
|
|
|
|
|
class TargetKind(StrEnum):
|
|
"""Command-Ziele (§6.3): All, ServerGroup, Node oder Output."""
|
|
|
|
ALL = "all"
|
|
SERVER_GROUP = "server_group"
|
|
NODE = "node"
|
|
OUTPUT = "output"
|
|
|
|
|
|
class GroupRule(StrEnum):
|
|
"""Zielregel je ServerGroup (§10.1)."""
|
|
|
|
ALL = "all"
|
|
SELECTED = "selected"
|
|
TAG_QUERY = "tag_query"
|
|
NODE_LIST = "node_list"
|
|
|
|
|
|
@dataclass
|
|
class ServerGroup:
|
|
"""Servergruppe (§10.1): Node-Auswahl mit Zielregel.
|
|
|
|
- node_ids: feste Mitglieder bei SELECTED/NODE_LIST
|
|
- tags: Node-Tags für TAG_QUERY (Node-Einträge tragen passende Tags)
|
|
- output_map: optionaler Layer-/Output-Zuordnungshinweis (§10.1)
|
|
"""
|
|
|
|
id: str
|
|
name: str
|
|
rule: GroupRule = GroupRule.SELECTED
|
|
node_ids: frozenset[str] = field(default_factory=frozenset)
|
|
tags: frozenset[str] = field(default_factory=frozenset)
|
|
output_map: dict[str, str] = field(default_factory=dict) # layer_id → output_id
|
|
|
|
|
|
class GroupRouter:
|
|
"""Löst Command-Ziele auf Node-Mengen auf (§6.3).
|
|
|
|
Der Coordinator nutzt resolve() vor jedem Senden; die UI kann
|
|
resolve() für die Commit-Vorschau verwenden (§17.5: „welche Nodes
|
|
erhalten dies?").
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._groups: dict[str, ServerGroup] = {}
|
|
self._node_tags: dict[str, frozenset[str]] = {} # node_id → tags
|
|
self._node_outputs: dict[str, list[str]] = {} # node_id → output_ids
|
|
|
|
# ---------- Verwaltung ----------
|
|
|
|
def upsert_group(self, group: ServerGroup) -> None:
|
|
self._groups[group.id] = group
|
|
|
|
def remove_group(self, group_id: str) -> None:
|
|
self._groups.pop(group_id, None)
|
|
|
|
def get_group(self, group_id: str) -> ServerGroup | None:
|
|
return self._groups.get(group_id)
|
|
|
|
def set_node_tags(self, node_id: str, tags: frozenset[str]) -> None:
|
|
self._node_tags[node_id] = frozenset(tags)
|
|
|
|
def set_node_outputs(self, node_id: str, output_ids: list[str]) -> None:
|
|
self._node_outputs[node_id] = list(output_ids)
|
|
|
|
def known_nodes(self) -> frozenset[str]:
|
|
return frozenset(self._node_tags)
|
|
|
|
# ---------- Zielauflösung (§6.3) ----------
|
|
|
|
def resolve(self, kind: TargetKind, target_id: str | None = None) -> frozenset[str]:
|
|
"""Löst ein Ziel auf eine Node-Menge auf; leer bei unbekanntem Ziel."""
|
|
if kind is TargetKind.ALL:
|
|
return self.known_nodes()
|
|
if kind is TargetKind.NODE:
|
|
return frozenset({target_id}) if target_id in self._node_tags else frozenset()
|
|
if kind is TargetKind.OUTPUT:
|
|
return frozenset(
|
|
node_id
|
|
for node_id, outputs in self._node_outputs.items()
|
|
if target_id in outputs
|
|
)
|
|
if kind is TargetKind.SERVER_GROUP:
|
|
group = self._groups.get(target_id or "")
|
|
if group is None:
|
|
return frozenset()
|
|
if group.rule is GroupRule.ALL:
|
|
return self.known_nodes()
|
|
if group.rule is GroupRule.SELECTED or group.rule is GroupRule.NODE_LIST:
|
|
return group.node_ids & self.known_nodes()
|
|
if group.rule is GroupRule.TAG_QUERY:
|
|
return frozenset(
|
|
node_id
|
|
for node_id, tags in self._node_tags.items()
|
|
if group.tags & tags
|
|
)
|
|
return frozenset()
|
|
|
|
def preview_targets(self, kind: TargetKind, target_id: str | None = None) -> frozenset[str]:
|
|
"""Commit-Vorschau: identisch zu resolve (§17.5)."""
|
|
return self.resolve(kind, target_id)
|