meldestelle/docs/01_Architecture/konzept-offline-first-desktop-backend-de.md

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

  1. Offline-First: Die Desktop-App ist voll funktionsfähig ohne Netzwerk; Synchronisation erfolgt opportunistisch.
  2. Event-isoliert: Pro Veranstaltung eigener Datenraum (gemäß ADR-0021). Keine Vermischung von Events.
  3. Einheitliches Änderungsmodell: Wiederverwendung des SyncEvent-Logs (ADR-0022) für Desktop↔Backend.
  4. Domänen-Mastership: Klare Schreibhoheiten reduzieren Konflikte, fachliche Regeln haben Vorrang vor rein technischen Timestamps.
  5. 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: schemaVersion ist Pflichtfeld für WAN-Sync (Schema-Evolution, Rolling Upgrades).
  • Persistenz: sync_events, sync_snapshots lokal (SQLDelight) und im Backend (pro Tenant-Schema) gespiegelt.

Lamport-Uhren & Vector-Clock (Optional)

  • Primär: Lamport-Uhren wie ADR-0022. Gleichstand → lexikografisch größere originNodeId gewinnt (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

  1. Strukturkonflikte (gleiches Aggregat, konkurrierende Events):
  • Wenn eine Seite nicht Master ist → Event wird angenommen, aber als PENDING_REVIEW markiert; 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 normale ErgebnisErfasst bei gleichem Lamport.
  1. Identitätskonflikte (provisionale Stammdaten):
  • Desktop darf temporäre Einträge (Temp-ID tmp-...) erzeugen.
  • Beim Push führt Backend Upsert+Merge aus, weist finale IDs zu und liefert IdMapping { tmpId -> finalId } zurück.
  • Desktop ersetzt Referenzen transaktional und emittiert ein lokales IdRemapped-Event (kein Re-Upload nötig, außer für Diagnose).
  1. Reihenfolge-/Kausalitätskonflikte:
  • Bei fehlenden Vorgänger-Events antwortet Backend mit rejected: [id] und requiredSinceSeq. Desktop zieht Delta (pull) und wiederholt den push.

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 über pull.
  • USB-Fallback (Notbetrieb): Export/Import von sync_events und sync_snapshots als verschlüsselte Archive (.msync). Offene Spezifikation; separater PoC.

Sicherheit

  • Mandantentrennung: Jeder Request trägt X-Event-Id (ADR-0021). Backend validiert gegen control.tenants.
  • Transport: https + mTLS (bevorzugt) oder https + OAuth2 Client Credentials. Payload-Signatur (HMAC-SHA256) empfohlen.
  • Integrität: checksum pro 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: minSupportedSchema aus hello; 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)

  1. Core: SyncEvent in Shared-KMP-Modul härten (schemaVersion), Persistenzschicht Desktop/Backend angleichen.
  2. Backend-API: hello/pull/push/snapshot Endpunkte implementieren (Spring Boot/Ktor), Mandantentrennung.
  3. Desktop-Client: Batch-Sync, Retry, Id-Mapping-Mechanismus.
  4. Review-UI: PENDING_REVIEW-Queue im Backend (Admin) und Anzeige im Desktop.
  5. E2E-Tests: Offline-Phase, Reconnect, Konflikt, Provisionals-Merge, Schema-Rolling-Upgrade.

Referenzen