diff --git a/docs/01_Architecture/adr/0022-lan-sync-protocol-de.md b/docs/01_Architecture/adr/0022-lan-sync-protocol-de.md new file mode 100644 index 00000000..c8b58caf --- /dev/null +++ b/docs/01_Architecture/adr/0022-lan-sync-protocol-de.md @@ -0,0 +1,315 @@ +--- +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). diff --git a/docs/04_Agents/Roadmaps/Curator_Roadmap.md b/docs/04_Agents/Roadmaps/Curator_Roadmap.md index 6960edd3..ba680c73 100644 --- a/docs/04_Agents/Roadmaps/Curator_Roadmap.md +++ b/docs/04_Agents/Roadmaps/Curator_Roadmap.md @@ -21,13 +21,18 @@ - [x] **B-0** | Rulebook-Session (03.04.2026) dokumentiert → `docs/99_Journal/2026-04-03_Rulebook_B1_Validierung_Frontend.md` +- [x] **B-1** (teilweise) | Architect B-1 Session-Log erstellt → + `docs/99_Journal/2026-04-03_Architect_B1_LAN-Sync_ADR-0022.md` +- [x] **B-1** (teilweise) | Roadmaps aktualisiert: Architect (✅ Sprint B), Backend (C-3 freigegeben), Frontend (C-3 + freigegeben) --- ## 🔴 Sprint B — Offen (höchste Priorität) - [ ] **B-1** | Roadmaps-Verzeichnis pflegen - - [ ] Alle 9 Roadmap-Dateien in `docs/04_Agents/Roadmaps/` auf Vollständigkeit prüfen ← *diese Session* + - [x] Architect-, Backend-, Frontend-Roadmaps aktualisiert (03.04.2026) + - [ ] Verbleibende Roadmaps (DevOps, QA, UI/UX, Rulebook) auf Vollständigkeit prüfen - [ ] Abgeschlossene Aufgaben als `[x]` markieren (nach Rückmeldung der Teams) - [ ] **B-2** | `docs/05_Backend/` aktualisieren diff --git a/docs/99_Journal/2026-04-03_Architect_B1_LAN-Sync_ADR-0022.md b/docs/99_Journal/2026-04-03_Architect_B1_LAN-Sync_ADR-0022.md new file mode 100644 index 00000000..2834ce5d --- /dev/null +++ b/docs/99_Journal/2026-04-03_Architect_B1_LAN-Sync_ADR-0022.md @@ -0,0 +1,73 @@ +# Session-Log: Architect B-1 — ADR-0022 LAN-Sync-Protokoll + +> **Datum:** 3. April 2026 +> **Agent:** 🏗️ Lead Architect +> **Aufgabe:** B-1 — ADR für LAN-Sync-Protokoll schreiben + +--- + +## Erledigte Aufgaben + +### 1. Optionen analysiert + +Drei Synchronisationsstrategien wurden für den Kontext Meldestelle ↔ Richter-Turm bewertet: + +| Option | Ergebnis | +|-----------------------------------------|--------------------------------------------------| +| A: Event-Sourcing (Append-Only Log) | Basis der gewählten Lösung (vereinfacht) | +| B: CRDT | Verworfen — zu komplex, keine KMP-Bibliotheken | +| C: Timestamp-Sync (Last-Write-Wins) | Verworfen — Uhren-Drift-Risiko, kein Audit-Trail | +| D: Event-Sourcing Light + Lamport-Uhren | **Gewählt** | + +### 2. Entscheidung getroffen + +**Event-Sourcing Light mit Lamport-Uhren** als LAN-Sync-Protokoll: + +- Meldestelle = Master für Nennungen/Kassa; Richter-Turm = Master für Bewertungen/Ergebnisse +- Domänen-Mastership eliminiert strukturell ~90 % der Konflikte +- Lamport-Timestamps lösen verbleibende Konflikte deterministisch (kein Uhren-Drift) +- WebSocket-Transport (bidirektional), Handshake via HELLO/HELLO_ACK/SYNC_DELTA +- Snapshots alle 100 Events begrenzen Log-Größe und Replay-Zeit + +### 3. ADR-0022 abgelegt + +**Datei:** `docs/01_Architecture/adr/0022-lan-sync-protocol-de.md` + +Inhalt: + +- Vollständige Optionsanalyse (A–D) +- `SyncEvent`-Datenmodell (Kotlin) +- Lamport-Uhr-Regeln +- WebSocket-Protokoll (Handshake, laufender Betrieb, Reconnect) +- Domänen-Mastership-Tabelle +- Snapshot-Strategie +- Implementierungsplan (4 Phasen) +- Offene Punkte (USB-Stick Fallback, Payload-Serialisierung, Multi-Richter-Turm) + +### 4. Downstream-Teams informiert + +- **👷 Backend Developer**: `Backend_Roadmap.md` — C-3 als freigegeben markiert, detaillierte Implementierungsschritte + ergänzt +- **🎨 Frontend Expert**: `Frontend_Roadmap.md` — C-3 als freigegeben markiert, WebSocket-Client- und UI-Aufgaben ergänzt +- **🏗️ Architect Roadmap**: Sprint B als abgeschlossen markiert + +--- + +## Geänderte Dateien + +| Datei | Änderung | +|----------------------------------------------------------------|------------------------------------------| +| `docs/01_Architecture/adr/0022-lan-sync-protocol-de.md` | **NEU** — ADR-0022 erstellt | +| `docs/04_Agents/Roadmaps/Architect_Roadmap.md` | Sprint B abgeschlossen, B-1 ✅ | +| `docs/04_Agents/Roadmaps/Backend_Roadmap.md` | C-3 freigegeben, Abhängigkeit ADR-0022 ✅ | +| `docs/04_Agents/Roadmaps/Frontend_Roadmap.md` | C-3 freigegeben, Abhängigkeit ADR-0022 ✅ | +| `docs/99_Journal/2026-04-03_Architect_B1_LAN-Sync_ADR-0022.md` | **NEU** — dieser Session-Log | + +--- + +## Nächste Schritte + +- **Sprint C-1** (Architect): Offline-First-Konzept für Desktop ↔ Backend ausarbeiten +- **Sprint C-3** (Backend): `SyncEvent`-Modell, SQLDelight-Tabellen, LamportClock, WebSocket-Server +- **Sprint C-3** (Frontend): WebSocket-Client, Sync-Status-UI, Discovery-UI +- **Offen**: USB-Stick Fallback — separate Besprechung (Sprint B/C)