fix: eigener Push ließ Client seinen Pull-Cursor über fremde Ereignisse springen

SyncEngine.PushAsync setzte den lokalen Pull-Cursor bisher aus PushResponse.ServerSequenceNr -
dem globalen Zähler über alle Geräte nach dem eigenen Push, nicht dem tatsächlich zugestellten
Stand. War beim Server zu diesem Zeitpunkt bereits ein noch nicht abgeholtes Ereignis eines
anderen Geräts mit niedrigerer ServerSeq vorhanden, sprang der Cursor darüber hinweg und der
direkt folgende Pull bekam 0 Ereignisse, ohne es je angewendet zu haben - ein zweiter,
unabhängiger Cursor-Bug mit demselben Symptom wie der vorherige Wasserzeichen-Fix, diesmal
client- statt serverseitig.

Ergänzt außerdem einen "Vollständigen Sync erzwingen"-Button in den Sync-Einstellungen, damit
bereits durch diesen Bug zu weit vorgerückte Geräte ihren Fortschritt manuell zurücksetzen und
alle Ereignisse erneut laden können.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 23:38:46 +02:00
co-authored by Claude Sonnet 5
parent 5bd6967421
commit e7e5faeba8
5 changed files with 138 additions and 4 deletions
+13 -3
View File
@@ -95,9 +95,19 @@ public class SyncEngine : IDisposable
foreach (var evt in pending)
if (result.AssignedServerSeqs.TryGetValue(evt.EventId, out var seq))
_queue.SetKnownServerSeq(evt.EntityType, evt.EntityId, seq);
_queue.SetLastServerSeq(result.ServerSequenceNr);
_logger?.Info($"Sync: Push - vom Server bestätigt bis ServerSequenceNr={result.ServerSequenceNr}, " +
$"{result.ConflictingEventIds.Count} vom Server abgelehnt (Konflikt).");
// ANDERS ALS FRÜHER wird der lokale Pull-Cursor (_queue.SetLastServerSeq) hier NICHT aus
// result.ServerSequenceNr gesetzt: das ist der GLOBALE Zähler über ALLE Geräte NACH diesem
// Push, nicht der Stand, den DIESES Gerät tatsächlich per Pull erhalten hat. Hatte der
// Server zu diesem Zeitpunkt bereits ein noch nicht abgeholtes Ereignis eines ANDEREN
// Geräts mit niedrigerer ServerSeq, würde der Cursor hier darüber hinwegspringen - der
// direkt anschließende PullAsync würde dann schon mit einem "since" danach fragen und 0
// Ereignisse zurückbekommen, ohne das fremde Ereignis je angewendet zu haben (exakt das
// Symptom, das die Ereignis-Anzahl-Korrektur in EventStore.Pull eigentlich beheben sollte -
// hier aber über einen anderen Pfad wieder hereinkam). PullAsync pflegt den Cursor bereits
// korrekt selbst, ausschließlich anhand tatsächlich zugestellter Ereignisse.
_logger?.Info($"Sync: Push - {pending.Count - result.ConflictingEventIds.Count} vom Server " +
$"angenommen (aktueller globaler Stand ServerSequenceNr={result.ServerSequenceNr}), " +
$"{result.ConflictingEventIds.Count} abgelehnt (Konflikt).");
if (result.ConflictingEventIds.Count > 0)
await HandleRejectedAsync(pending.Where(e => result.ConflictingEventIds.Contains(e.EventId)));
return (pending.Count - result.ConflictingEventIds.Count,