8.4 KiB
| type | status | owner | last_update |
|---|---|---|---|
| Concept | DRAFT | Lead Architect | 2026-04-03 |
Konzept: Offline-First Synchronisation (Desktop ↔ Backend)
Ziel und Rahmen
Dieses Dokument definiert das Synchronisations-Konzept zwischen der Compose Desktop App (Meldestelle-Zentrale) und dem Backend in einem Offline-First Szenario. Es baut auf ADR-0021 (Tenant-Isolation) und ADR-0022 (LAN-Sync mit Lamport-Uhren) auf und erweitert sie um die WAN/Backend-Synchronisation.
Nicht-Ziele: Cloud-Realtime für Endnutzer, kollaboratives Editieren außerhalb des Veranstaltungsbetriebs.
Leitprinzipien
- Offline-First: Die Desktop-App ist voll funktionsfähig ohne Netzwerk; Synchronisation erfolgt opportunistisch.
- Event-isoliert: Pro Veranstaltung eigener Datenraum (gemäß ADR-0021). Keine Vermischung von Events.
- Einheitliches Änderungsmodell: Wiederverwendung des
SyncEvent-Logs (ADR-0022) für Desktop↔Backend. - Domänen-Mastership: Klare Schreibhoheiten reduzieren Konflikte, fachliche Regeln haben Vorrang vor rein technischen Timestamps.
- Deterministische Konfliktauflösung: Lamport-Uhren + Regel-Matrix; keine Abhängigkeit von Systemuhren.
Topologie & Rollen
- Backend (Zentrale Plattform):
- Master für: Stammdaten (Reiter, Pferde, Vereine, Funktionäre), Identität/Rollen, Gebührenkataloge, globale Referenzen.
- Aggregations-/Archiv-Quelle nach Veranstaltungsende (finale Ergebnisse, Abrechnungen).
- Desktop (Meldestelle-Zentrale):
- Master während der Veranstaltung für: Nennungen (operativ), Startreihenfolgen, Startlisten-Status, Ergebnisse/Protokolle (falls Richter nicht direkt am Backend), Kassa-Operationen vor Ort.
- Hält lokales
SyncEvent-Log + Snapshots (vgl. ADR-0022) und synchronisiert mit Backend, sobald Konnektivität besteht.
Hinweis Mehrfach-Desktops: Genau eine „Zentrale“ pro Veranstaltung besitzt Schreibhoheit (Konfig-Flag isEventAuthority=true). Weitere Geräte sind Replikate/Clients.
Datenkategorien & Mastership
| Kategorie | Master | Desktop Rechte | Backend Rechte |
|---|---|---|---|
| Stammdaten (Actor) | Backend | Lesen, lokal „provisional“ anlegen (Temp-ID) | Vollzugriff, ID-Zuteilung, Merge |
| Veranstaltungs-Stammdaten | Backend | Lesen | Vollzugriff |
| Nennungen operativ | Desktop | Vollzugriff | Lesen, Import nach Sync |
| Startreihenfolge/Status | Desktop | Vollzugriff | Lesen, Import nach Sync |
| Bewertungen/Ergebnisse | Desktop/Richter | Vollzugriff (Eventzeitraum) | Lesen, Publikation/Archiv |
| Kassa/Finanzen vor Ort | Desktop | Vollzugriff | Lesen, Abgleich Summen |
Konflikte über Kategoriegrenzen werden durch Mastership-Regeln verhindert; verbleibende Konflikte werden per Regel-Matrix gelöst.
Änderungsmodell (Wiederverwendung SyncEvent)
Struktur wie in ADR-0022 beschrieben:
data class SyncEvent(
val eventId: String,
val turnierId: String?,
val sequenceNumber: Long, // Lamport
val originNodeId: String, // Desktop-ID oder Backend-Node-ID
val aggregateType: String, // z. B. "Nennung", "Bewertung", "Start"
val aggregateId: String,
val eventType: String,
val payload: ByteArray,
val createdAt: Instant,
val checksum: String,
val schemaVersion: Int,
)
- Erweiterung:
schemaVersionist Pflichtfeld für WAN-Sync (Schema-Evolution, Rolling Upgrades). - Persistenz:
sync_events,sync_snapshotslokal (SQLDelight) und im Backend (pro Tenant-Schema) gespiegelt.
Lamport-Uhren & Vector-Clock (Optional)
- Primär: Lamport-Uhren wie ADR-0022. Gleichstand → lexikografisch größere
originNodeIdgewinnt (Determinismus). - Optional für feingranulare Erkennung: Per-Aggregat Vector-Clock (
Map<nodeId, lamport>) zur Diagnose; Entscheidungsgrundlage bleibt Lamport + Fachregeln.
Sync-Protokoll Desktop ↔ Backend (HTTPS)
Transport: HTTPS (HTTP/2), JSON oder Protobuf, idempotente Endpunkte. Auth: mTLS zwischen Desktop und Backend ODER OAuth2 Client Credentials + Signatur der Batch-Payload.
Empfohlene Endpunkte (pro eventId):
POST /api/sync/{eventId}/hello → { nodeId, lastKnownSeq } → { backendNodeId, currentSeq, minSupportedSchema }
POST /api/sync/{eventId}/pull → { sinceSeq, limit } → [ SyncEvent... ], { nextSeq }
POST /api/sync/{eventId}/push → [ SyncEvent... ] → { ackedMaxSeq, rejected:[ids...] }
POST /api/sync/{eventId}/snapshot/request → { scope } → { snapshotBlob, snapshotSeq }
POST /api/sync/{eventId}/diagnostics → { stats } → { advice }
Batching: bis 512 Events oder 1 MiB pro Batch. Serverseitiges Paging über sinceSeq/nextSeq.
Idempotenz: Jeder SyncEvent wird durch (eventId, originNodeId, sequenceNumber, checksum) dedupliziert.
Konfliktauflösung
- Strukturkonflikte (gleiches Aggregat, konkurrierende Events):
- Wenn eine Seite nicht Master ist → Event wird angenommen, aber als
PENDING_REVIEWmarkiert; fachliche Entscheidung erforderlich (Backend-UI oder Desktop-Review-Queue). - Beide Master (Sonderfälle, z. B. Ergebnisse während parallelem Backend-Fix):
- Lamport höher gewinnt.
- Gleichstand →
originNodeId-Tiebreaker. - Zusätzlich fachliche Heuristik optional: „Korrektur-Events“ (z. B.
ErgebnisKorrigiert) schlagen normaleErgebnisErfasstbei gleichem Lamport.
- Identitätskonflikte (provisionale Stammdaten):
- Desktop darf temporäre Einträge (Temp-ID
tmp-...) erzeugen. - Beim Push führt Backend
Upsert+Mergeaus, weist finale IDs zu und liefertIdMapping { tmpId -> finalId }zurück. - Desktop ersetzt Referenzen transaktional und emittiert ein lokales
IdRemapped-Event (kein Re-Upload nötig, außer für Diagnose).
- Reihenfolge-/Kausalitätskonflikte:
- Bei fehlenden Vorgänger-Events antwortet Backend mit
rejected: [id]undrequiredSinceSeq. Desktop zieht Delta (pull) und wiederholt denpush.
Snapshots & Recovery
- Snapshot-Intervall: standardmäßig 100 Events pro
(aggregateType, scope)(wie ADR-0022), für WAN-Sync zusätzlich Full-State-Snapshot pro Veranstaltung vor Event-Abschluss. - Recovery: Desktop kann mit leerem Log starten →
snapshot/request→ Full-State +snapshotSeq→ weitere Deltas überpull. - USB-Fallback (Notbetrieb): Export/Import von
sync_eventsundsync_snapshotsals verschlüsselte Archive (.msync). Offene Spezifikation; separater PoC.
Sicherheit
- Mandantentrennung: Jeder Request trägt
X-Event-Id(ADR-0021). Backend validiert gegencontrol.tenants. - Transport:
https+ mTLS (bevorzugt) oderhttps+ OAuth2 Client Credentials. Payload-Signatur (HMAC-SHA256) empfohlen. - Integrität:
checksumpro Event wird serverseitig geprüft; Mismatch → Reject. - Rechte: Backend erzwingt Mastership-Regeln serverseitig; Verstöße →
PENDING_REVIEW+ Audit-Log.
Fehlerfälle & Resilienz
- Netzwerkfehler: Exponentielles Backoff (bis 5 min), Offline-Queue unbegrenzt (bounded by disk quota), Telemetrie im UI.
- Schema-Divergenz:
minSupportedSchemaaushello; Desktop migriert vor weiterem Sync oder schaltet in Read-Only. - Duplikate: Idempotenz-Keys verhindern Doppelverarbeitung. ACK enthält höchste verarbeitete
sequenceNumber.
Observability
- Metriken:
sync_push_events_total,sync_pull_events_total,sync_rejected_total,sync_latency_ms(p50/p95),offline_duration_s. - Logs: pro Event
tenant,originNodeId,seq,aggType,eventType,result. - UI: Status-Anzeige (Verbunden, Getrennt, Ausstehend X), Konflikt-Review-Queue.
Einführungsplan (Auszug)
- Core:
SyncEventin Shared-KMP-Modul härten (schemaVersion), Persistenzschicht Desktop/Backend angleichen. - Backend-API:
hello/pull/push/snapshotEndpunkte implementieren (Spring Boot/Ktor), Mandantentrennung. - Desktop-Client: Batch-Sync, Retry, Id-Mapping-Mechanismus.
- Review-UI:
PENDING_REVIEW-Queue im Backend (Admin) und Anzeige im Desktop. - E2E-Tests: Offline-Phase, Reconnect, Konflikt, Provisionals-Merge, Schema-Rolling-Upgrade.