--- type: ADR status: ACCEPTED owner: Lead Architect last_update: 2026-04-03 --- # ADR-0022: LAN-Sync-Protokoll (Meldestelle ↔ Richter-Turm) ## Status Akzeptiert — 2026-04-03 --- ## Kontext Die Desktop-Anwendung "Meldestelle-Biest" operiert auf Reitturnieren in einer LAN-Umgebung ohne garantierte Internetverbindung. Zwei Hauptakteure müssen Daten in Echtzeit synchronisieren: - **Meldestelle-Desk** (Schreibzentrale): Verwaltet Nennungen, Startreihenfolgen, Teilnehmerkonten und Kassendaten. - **Richter-Turm-Desk** (Leseintensiv, gelegentlich schreibend): Zeigt Startreihenfolgen an, erfasst Bewertungen und Ergebnisse. ### Problemstellung Welches Synchronisationsprotokoll eignet sich am besten für folgende Anforderungen? 1. **Offline-First**: Beide Seiten müssen auch ohne aktive Verbindung arbeitsfähig bleiben. 2. **Konfliktauflösung**: Gleichzeitige Änderungen (z. B. Meldestelle ändert Startnummer, Richter erfasst Ergebnis) müssen deterministisch aufgelöst werden. 3. **Einfachheit**: Das System wird von 1–3 Entwicklern betreut; Komplexität muss beherrschbar bleiben. 4. **Latenz**: Ergebnisübertragung vom Richter-Turm zur Meldestelle soll < 500 ms betragen. 5. **Datenmenge**: Pro Turnier typisch 50–300 Starts; kein Big-Data-Problem. --- ## Analyse der Optionen ### Option A: Event-Sourcing (Append-Only Event Log) **Konzept:** Alle Zustandsänderungen werden als unveränderliche Events in einem Log gespeichert. Der aktuelle Zustand ergibt sich durch Replay aller Events. Peers tauschen fehlende Events aus (Log-Replikation). **Vorteile:** - Vollständige Audit-Trail aller Änderungen. - Natürliche Konfliktauflösung durch Event-Reihenfolge (Sequenznummern). - Gut kombinierbar mit CQRS. - Einfaches Nachsynchronisieren: Peer sendet seinen letzten bekannten `sequence_number`, Gegenseite liefert Delta. **Nachteile:** - Log wächst unbegrenzt (Snapshots erforderlich). - Höhere Implementierungskomplexität (Projektion, Snapshot-Strategie). - Replay bei großem Log kann langsam sein (mitigierbar durch Snapshots). **Bewertung für unseren Kontext:** Gut geeignet, aber Snapshot-Strategie erhöht Komplexität. Für 50–300 Starts pro Turnier ist der Log überschaubar. --- ### Option B: CRDT (Conflict-free Replicated Data Types) **Konzept:** Spezielle Datenstrukturen, die mathematisch garantieren, dass parallele Änderungen immer zu einem konsistenten Zustand konvergieren — ohne zentrale Koordination. **Vorteile:** - Echte Peer-to-Peer-Fähigkeit ohne Master. - Automatische, mathematisch korrekte Konfliktauflösung. - Keine Koordination nötig. **Nachteile:** - Sehr hohe Implementierungskomplexität (LWW-Register, OR-Set, RGA für Texte etc.). - Kaum Kotlin/KMP-Bibliotheken verfügbar; müsste selbst implementiert werden. - Semantische Konflikte (z. B. "Startnummer geändert UND Ergebnis für diese Startnummer erfasst") sind mit CRDTs nicht lösbar — nur strukturelle Konflikte. - Debugging und Nachvollziehbarkeit schwierig. **Bewertung für unseren Kontext:** Überdimensioniert und zu komplex für ein 1–3-Personen-Team. Verworfen. --- ### Option C: Timestamp-basierte Synchronisation (Last-Write-Wins) **Konzept:** Jede Entität trägt einen `updated_at`-Timestamp. Bei Konflikten gewinnt die neuere Änderung. Peers tauschen Deltas seit einem bekannten Zeitpunkt aus. **Vorteile:** - Sehr einfach zu implementieren. - Gut verständlich und debuggbar. - Funktioniert mit bestehenden SQLDelight-Schemas ohne große Umbauten. **Nachteile:** - Uhren-Drift zwischen Geräten kann zu falschen Ergebnissen führen (mitigierbar durch NTP oder logische Uhren). - Kein vollständiger Audit-Trail. - "Last-Write-Wins" kann fachlich falsch sein (z. B. ältere Bewertung überschreibt neuere Korrektur). - Keine Möglichkeit, Änderungshistorie nachzuvollziehen. **Bewertung für unseren Kontext:** Zu riskant für fachlich kritische Daten (Ergebnisse, Nennungen). Uhren-Drift im LAN-Betrieb ohne NTP ist ein reales Problem. Verworfen als alleinige Strategie. --- ### Option D (Gewählt): Hybridansatz — Event-Sourcing Light mit Lamport-Uhren **Konzept:** Vereinfachtes Event-Sourcing ohne vollständigen CQRS-Stack, kombiniert mit logischen Uhren (Lamport Timestamps) statt Wanduhren. Jede Änderung erzeugt ein `SyncEvent` mit monoton steigender logischer Uhr. Peers tauschen fehlende Events via WebSocket aus (Push bei Verbindung, Pull bei Reconnect). **Kernprinzipien:** 1. **Meldestelle ist Master** für Nennungs- und Kassendaten. 2. **Richter-Turm ist Master** für Bewertungs- und Ergebnisdaten. 3. **Klare Domänentrennung** eliminiert 90 % der Konflikte strukturell. 4. **Lamport-Timestamps** lösen verbleibende Konflikte deterministisch ohne Uhren-Drift-Problem. 5. **Snapshots** alle N Events (konfigurierbar, Standard: 100) begrenzen Log-Größe und Replay-Zeit. --- ## Entscheidung Wir implementieren **Option D: Event-Sourcing Light mit Lamport-Uhren** als LAN-Sync-Protokoll. ### Architektur-Überblick ``` ┌─────────────────────────────┐ WebSocket (LAN) ┌─────────────────────────────┐ │ Meldestelle-Desk │◄──────────────────────────────►│ Richter-Turm-Desk │ │ │ │ │ │ Master für: │ SyncEvent-Stream │ Master für: │ │ - Nennungen │ (bidirektional) │ - Bewertungen │ │ - Startreihenfolge │ │ - Ergebnisse │ │ - Teilnehmerkonten │ │ - Richter-Notizen │ │ - Kassendaten │ │ │ │ │ │ │ │ SQLDelight (lokal) │ │ SQLDelight (lokal) │ │ + SyncEvent-Log │ │ + SyncEvent-Log │ └─────────────────────────────┘ └──────────────────────────────┘ ``` ### SyncEvent-Datenmodell ```kotlin data class SyncEvent( val eventId: String, // Veranstaltungs-ID (Tenant) val turnierId: String, // Turnier-Scope (gemäß ADR-0020) val sequenceNumber: Long, // Lamport-Timestamp (logische Uhr) val originNodeId: String, // Geräte-ID (UUID, persistent) val aggregateType: String, // z. B. "Nennung", "Bewertung", "Start" val aggregateId: String, // ID der betroffenen Entität val eventType: String, // z. B. "NennungErstellt", "BewertungErfasst" val payload: ByteArray, // JSON/Protobuf-serialisiertes Delta val createdAt: Instant, // Wanduhr (nur für Anzeige/Logging) val checksum: String // SHA-256 des Payloads (Integritätsprüfung) ) ``` ### Lamport-Uhr-Regeln ``` Bei lokaler Änderung: localClock = localClock + 1 Bei Empfang eines Events: localClock = max(localClock, event.sequenceNumber) + 1 Konflikt-Auflösung: Höherer sequenceNumber gewinnt. Bei Gleichstand: lexikografisch größere originNodeId gewinnt (deterministisch). ``` ### Sync-Protokoll (WebSocket) #### Verbindungsaufbau (Handshake) ``` Client → Server: HELLO { nodeId, eventId, turnierId, lastKnownSeq } Server → Client: HELLO_ACK { nodeId, currentSeq } Server → Client: SYNC_DELTA [ SyncEvent, ... ] (alle Events > lastKnownSeq) Client → Server: SYNC_ACK { ackedSeq } ``` #### Laufender Betrieb ``` Änderung lokal: SYNC_PUSH { SyncEvent } Gegenseite: SYNC_ACK { ackedSeq } oder SYNC_NACK { reason } Heartbeat: PING / PONG alle 30 Sekunden ``` #### Reconnect / Offline-Recovery ``` Reconnect: HELLO { lastKnownSeq } → Server liefert Delta seit lastKnownSeq Snapshot-Request: SNAPSHOT_REQUEST { turnierId } → Server liefert aktuellen Snapshot + seq ``` ### Domänen-Mastership (Konflikt-Prävention) | Aggregat | Master | Richter-Turm darf | |---------------------|------------------|-------------------| | Nennung | Meldestelle | Lesen | | Startreihenfolge | Meldestelle | Lesen | | TeilnehmerKonto | Meldestelle | Lesen | | Kassenbuchung | Meldestelle | Kein Zugriff | | Bewertung | Richter-Turm | Schreiben | | Ergebnis | Richter-Turm | Schreiben | | Richter-Notiz | Richter-Turm | Schreiben | | Veranstaltungs-Chat | Beide (CRDT-Set) | Schreiben | > **Hinweis:** Schreibt ein Peer in ein Aggregat, für das er nicht Master ist, wird das Event lokal gespeichert > aber mit `status = PENDING_REVIEW` markiert und dem Master zur Bestätigung vorgelegt. ### Snapshot-Strategie - Snapshot wird nach jeweils **100 Events** pro `(turnierId, aggregateType)` erstellt. - Snapshot enthält: vollständiger Zustand des Aggregats + `snapshotSeq`. - Beim Replay: Lade letzten Snapshot, wende nur Events mit `seq > snapshotSeq` an. - Snapshots werden lokal in SQLDelight gespeichert und bei Bedarf übertragen. ### Sicherheit (gemäß ADR-0020) - WebSocket-Verbindung via `wss://` mit Shared Security Key pro Veranstaltung. - `eventId` im Handshake wird gegen lokale Registry validiert. - Events mit falscher `eventId` oder `turnierId` werden verworfen (kein Silent-Drop, sondern `SYNC_NACK`). --- ## Konsequenzen ### Positiv - **Offline-First**: Beide Peers arbeiten vollständig lokal; Sync erfolgt opportunistisch bei Verbindung. - **Kein Uhren-Drift-Problem**: Lamport-Uhren sind unabhängig von Systemuhren. - **Audit-Trail**: Vollständige Änderungshistorie pro Turnier nachvollziehbar. - **Einfaches Delta-Sync**: `lastKnownSeq` reicht für vollständiges Nachsynchronisieren. - **Klare Verantwortlichkeiten**: Domänen-Mastership eliminiert strukturell die meisten Konflikte. - **Skalierbar**: Funktioniert für 1 Richter-Turm ebenso wie für 5 parallele Türme. ### Negativ / Herausforderungen - **Log-Management**: Snapshot-Strategie muss implementiert und getestet werden. - **Schema-Evolution**: Payload-Format muss versioniert werden (empfohlen: JSON mit `schemaVersion`-Feld). - **Mehr Aufwand als reines Timestamp-Sync**: Initiale Implementierung aufwändiger, langfristig robuster. - **WebSocket-Infrastruktur**: Meldestelle-Desk fungiert als WebSocket-Server; Richter-Turm als Client (gemäß ADR-0020 Master-Client-Hybrid). ### Neutral - SQLDelight-Schema muss um `sync_events`- und `sync_snapshots`-Tabellen erweitert werden. - `originNodeId` muss beim ersten Start persistent generiert und gespeichert werden. --- ## Betrachtete Alternativen | Option | Entscheidung | Hauptgrund | |---------------------|---------------------|-----------------------------------------------------------------| | A: Event-Sourcing | Basis (vereinfacht) | Zu komplex mit vollem CQRS-Stack; Light-Variante gewählt | | B: CRDT | Verworfen | Zu hohe Implementierungskomplexität, keine KMP-Bibliotheken | | C: Timestamp-Sync | Verworfen | Uhren-Drift-Risiko, kein Audit-Trail, fachlich unsicher | | D: Hybrid (gewählt) | Akzeptiert | Beste Balance aus Robustheit, Einfachheit und Offline-Fähigkeit | --- ## Implementierungsplan ### Phase 1 — Fundament (Backend Sprint B / Frontend Sprint B) - [ ] `SyncEvent`-Datenmodell in `core`-Modul definieren (KMP-shared) - [ ] SQLDelight-Tabellen `sync_events`, `sync_snapshots` anlegen - [ ] `LamportClock`-Implementierung (thread-safe, persistent) - [ ] `originNodeId`-Generierung und -Persistierung beim App-Start ### Phase 2 — Transport (Backend Sprint B / Frontend Sprint B) - [ ] WebSocket-Server auf Meldestelle-Desk (Ktor) - [ ] WebSocket-Client auf Richter-Turm-Desk (Ktor-Client / KMP) - [ ] Handshake-Protokoll implementieren (HELLO / HELLO_ACK / SYNC_DELTA) - [ ] SYNC_PUSH / SYNC_ACK / PING-PONG implementieren ### Phase 3 — Konfliktauflösung & Recovery - [ ] Domänen-Mastership-Validierung im Event-Handler - [ ] Snapshot-Erstellung und -Wiederherstellung - [ ] Reconnect-Logik mit Delta-Sync (lastKnownSeq) - [ ] `PENDING_REVIEW`-Workflow für Mastership-Verletzungen ### Phase 4 — Observability & Tests - [ ] Sync-Status-UI (verbunden / getrennt / ausstehende Events) - [ ] Integrationstests: 2 Peers, Offline-Phase, Reconnect, Konflikt - [ ] Chaos-Tests: Verbindungsabbruch während Sync, Clock-Skew-Simulation --- ## Offene Punkte - **USB-Stick Fallback**: Separate Besprechung (Sprint B/C) — Export/Import des SyncEvent-Logs auf USB als Notfall-Sync-Kanal. Snapshot-Format ist bereits kompatibel. - **Payload-Serialisierung**: JSON (einfacher) vs. Protobuf (kompakter). Empfehlung: JSON mit `schemaVersion` für Phase 1, Migration zu Protobuf optional in Phase 2. - **Mehrere Richter-Türme**: Protokoll unterstützt N Clients; Broadcast-Strategie auf Meldestelle-Seite noch zu spezifizieren (Fan-out vs. Relay). --- ## Referenzen - [ADR-0020: LAN-Kommunikation und Daten-Isolierung](0020-lan-communication-isolation-de.md) - [ADR-0021: Tenant-Resolution-Strategie](0021-tenant-resolution-strategy-de.md) - [ADR-0004: Event-driven Communication](0004-event-driven-communication-de.md) - [ADR-0010: SQLDelight for Cross-Platform Persistence](0010-sqldelight-for-cross-platform-persistence.md) - [ADR-0008: Multiplatform Client Applications](0008-multiplatform-client-applications-de.md) - Lamport, L. (1978). "Time, Clocks, and the Ordering of Events in a Distributed System." CACM 21(7).