fix: Pull-Wasserzeichen sprang über fremde Ereignisse + schärfere Push-Kollisionskontrolle
EventStore.Pull gab bisher den globalen ServerSeq-Höchststand als neuen Cursor zurück statt den höchsten unter den tatsächlich gelieferten Ereignissen - hatte ein Gerät selbst kurz zuvor etwas gepusht, sprang sein Pull-Cursor über noch nicht abgeholte Ereignisse anderer Geräte hinweg und verpasste sie dauerhaft, ohne jeden Fehler. Ersetzt außerdem die bisherige 30-Sekunden-Heuristik zur Konflikterkennung beim Push durch exakte BasedOnServerSeq-Prüfung: jedes SyncEvent trägt die ServerSeq, auf der es aufbaut: der Server lehnt ab, wenn der aktuelle Stand nicht mehr passt. Bei Ablehnung lädt der Client sofort den neuen Server-Stand nach, löst den Konflikt nach der bestehenden Desktop-vs-Companion/ Timestamp-Politik auf und macht ihn immer in der Konflikt-Review-UI sichtbar, statt die verworfene Änderung stillschweigend zu verlieren. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1557,6 +1557,32 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba
|
||||
nächsten Testlauf anhand der Log-Dateien beider Geräte lückenlos nachvollziehen, an welcher
|
||||
Stelle der Kette (Einreihen → Push → Server → Pull → Anwenden) eine Änderung tatsächlich
|
||||
verloren geht — bisher war das reine Spekulation ohne Live-Testgeräte.
|
||||
|
||||
**Nachtrag (der eigentliche Bugfix — server-seitig in `EventStore.Pull`):** Die
|
||||
Erfolgs-Protokollierung von oben hat sofort den Übeltäter gezeigt: Gerät A pusht 2 Ereignisse,
|
||||
Server bestätigt (`ServerSequenceNr=420`, 0 Konflikte) — Gerät B pullt direkt danach und
|
||||
bekommt "0 Ereignisse" zurück, obwohl Geräts A's Ereignisse eindeutig für Gerät B bestimmt
|
||||
waren. Ursache: `EventStore.Pull()` gab als neuen "since"-Cursor bisher IMMER
|
||||
`LastSeq(col)` zurück — den **globalen** Höchststand über ALLE Geräte hinweg, nicht die
|
||||
höchste ServerSeq unter den tatsächlich in `Events` zurückgegebenen (nach
|
||||
`DeviceId != requestingDeviceId` gefilterten) Ereignissen. `SyncEngine.PullAsync` übernimmt
|
||||
diesen Wert 1:1 als neuen Cursor für den nächsten Pull. Hatte das anfragende Gerät selbst kurz
|
||||
zuvor etwas gepusht (Push läuft in `SyncNowAsync` immer vor Pull — die eigenen Ereignisse
|
||||
werden aus der Pull-Antwort korrekt herausgefiltert, da man sie nicht noch mal auf sich selbst
|
||||
anwenden will), sprang der Cursor über die noch gar nicht abgeholten Ereignisse ANDERER Geräte
|
||||
hinweg, sobald die eigenen neuer waren — sie wurden **dauerhaft** verpasst, ohne jeden
|
||||
Fehler, da aus Sicht des Clients ein leeres Pull-Ergebnis ein völlig normaler, erfolgreicher
|
||||
Zustand ist ("nichts Neues").
|
||||
|
||||
**Fix:** `ServerSequenceNr` in der Pull-Antwort ist jetzt `events.Count > 0 ?
|
||||
events.Max(e => e.SequenceNr) : since` — der Cursor rückt nur noch so weit vor, wie
|
||||
tatsächlich Ereignisse ausgeliefert wurden, nie darüber hinaus. Neuer Regressionstest
|
||||
`EventStoreTests.Pull_AnfragendesGeraetHatSelbstNeuereEreignisseGepusht_
|
||||
UeberspringtFremdeEreignisseNicht` reproduziert exakt dieses Szenario (Gerät A pusht,
|
||||
dann pusht Gerät B selbst etwas Neueres, dann pullt Gerät B mit einem alten "since") und
|
||||
belegt, dass Geräts A's Ereignis jetzt zurückkommt und der neue Cursor bei dessen ServerSeq
|
||||
steht statt beim (höheren) globalen Höchststand. **Wichtig:** betrifft `LehrerApp.Api` — ein
|
||||
Server-Redeploy ist diesmal nötig (kein reiner Desktop-Client-Fix).
|
||||
- [x] **10.1.8** Datei-Anhänge (Dokumentation) über den laufenden Sync mitschicken.
|
||||
|
||||
**Umsetzung:** Eigener, unverschlüsselt im JSON-Ereigniskanal nicht mitgeführter Binärkanal
|
||||
@@ -1567,6 +1593,52 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba
|
||||
Anhänge nach Anwenden eines `Documentation`-Ereignisses). Original-`StorageId` bleibt beim
|
||||
Download erhalten (roher `db.Attachments.Upload`-Aufruf statt `IAttachmentStorage.Upload`,
|
||||
das immer eine neue Id vergäbe).
|
||||
- [x] **10.1.9** Schärfere Kollisionskontrolle beim Push (Konzeptgespräch nach dem
|
||||
Pull-Watermark-Bugfix, siehe Nachtrag zu 10.1.7): statt der bisherigen 30-Sekunden-
|
||||
Heuristik ("hat ein anderes Gerät kürzlich dieselbe Entität angefasst") trägt jedes
|
||||
`SyncEvent` jetzt ein `BasedOnServerSeq` — die ServerSeq, auf der die lokale Änderung
|
||||
aufbaut (`null` = Entität hier noch nie synchronisiert).
|
||||
|
||||
**Diskutierter Alternativvorschlag (verworfen):** Client-Zeitstempel (UTC-Ticks) statt
|
||||
server-vergebener ServerSeq als Sync-Cursor. Verworfen, weil unsicher für Offline-First:
|
||||
ein tagelang offline gewesenes Gerät würde beim Reconnect "alte" logische Zeitstempel
|
||||
pushen; jedes andere Gerät, dessen Cursor während der Offline-Zeit bereits über diesen
|
||||
Wall-Clock-Zeitpunkt hinausgelaufen ist, würde die Änderung dauerhaft und lautlos
|
||||
überspringen. Der als Konfliktmarker vorgeschlagene Teil der Idee ("Stand-vor-Änderung
|
||||
mitschicken") war dagegen richtig und ist die Grundlage von `BasedOnServerSeq` geworden.
|
||||
|
||||
**Umsetzung:**
|
||||
- Server (`EventStore.Push`): nimmt ein Ereignis nur an, wenn `BasedOnServerSeq` exakt der
|
||||
aktuellen ServerSeq der Entität entspricht (`GetLatestForEntity`/`LatestForEntity`);
|
||||
liefert je akzeptiertem Ereignis die neu vergebene ServerSeq in `AssignedServerSeqs`
|
||||
zurück. Neuer Endpunkt `GET /api/sync/entity/{entityType}/{entityId}` liefert den
|
||||
aktuellen Server-Stand einer einzelnen Entität.
|
||||
- Client (`EventQueue`): neue lokale Versionsverfolgung je Entität
|
||||
(`GetKnownServerSeq`/`SetKnownServerSeq`, Collection `entity_versions`) — aktualisiert
|
||||
sowohl beim Anwenden eingehender Ereignisse (`EventApplier.ApplyAsync`) als auch nach
|
||||
erfolgreichem Push (`AssignedServerSeqs`).
|
||||
- Client (`SyncEngine.PushAsync`): dedupliziert mehrere ausstehende Ereignisse derselben
|
||||
Entität vor dem Senden auf das jüngste (Payload ist immer ein vollständiger Snapshot,
|
||||
nie ein Delta — ältere Duplikate sind redundant und würden mit demselben, dann
|
||||
veralteten `BasedOnServerSeq` unnötig kollidieren). `BasedOnServerSeq` wird erst
|
||||
unmittelbar vor dem Senden aus der lokalen Versionsverfolgung gesetzt, nicht beim
|
||||
Einreihen — zwischen Enqueue und Push kann ein Pull den bekannten Stand bereits
|
||||
aktualisiert haben.
|
||||
- **Ablehnungsbehandlung (Nutzer-Vorgabe: "Änderungen verwerfen ist das eine, aber man
|
||||
sollte es wissen"):** bei einer Ablehnung wird der aktuelle Server-Stand sofort über den
|
||||
neuen Endpunkt nachgeladen (statt auf den nächsten regulären Pull zu warten), nach
|
||||
derselben Desktop-schlägt-Companion/neuerer-Timestamp-Politik wie `ConflictResolver`
|
||||
aufgelöst (`ConflictResolver.DetermineWinner`, aus `TryResolve` herausgezogen, damit
|
||||
Pull- und Push-Konflikte dieselbe Regel nutzen) und **immer** als `ConflictEntry` in der
|
||||
bestehenden Konflikt-Review-UI (10.1.6) sichtbar gemacht — unabhängig davon, welche Seite
|
||||
gewinnt. Bei "RemoteWon" wird der Server-Stand sofort angewendet und die lokale Änderung
|
||||
verworfen; bei "LocalWon" bleibt das Ereignis unbestätigt in der Queue und wird beim
|
||||
nächsten Sync-Versuch mit dem nun aktualisierten `BasedOnServerSeq` automatisch erneut
|
||||
versucht.
|
||||
- Neue Tests: `EventStoreTests` (Annahme/Ablehnung nach `BasedOnServerSeq`,
|
||||
`AssignedServerSeqs`, `GetLatestForEntity`), `EventQueueTests`
|
||||
(`GetKnownServerSeq`/`SetKnownServerSeq`), `SyncEngineTests` (Dedup, frisch gesetztes
|
||||
`BasedOnServerSeq`, Versionsverfolgung nach Erfolg, RemoteWon- und LocalWon-Ablehnung).
|
||||
|
||||
### 10.2 Server
|
||||
- [x] **10.2.1** Benutzerverwaltung/Registrierung prüfen und absichern
|
||||
|
||||
Reference in New Issue
Block a user