fix: Sync blockierte dauerhaft und unsichtbar bei fehlerhaftem Ereignis

EventApplier.ApplyAsync fing bisher nur LiteException ab - jede andere
Ausnahme (z.B. eine fehlerhafte Entschlüsselung/Deserialisierung eines
einzelnen Ereignisses) fiel unbehandelt aus der Pull-Schleife in
SyncEngine.PullAsync heraus, bevor der Fortschritt (SetLastServerSeq)
gespeichert wurde. Der nächste Sync-Versuch lud denselben Batch erneut
und scheiterte am selben Ereignis wieder - ein dauerhaft blockierter
Sync, bei dem selbst bereits erfolgreich angewendete Ereignisse im
selben Batch nie als erledigt markiert wurden. Zusätzlich protokollierte
weder SyncEngine noch EventApplier irgendetwas, ein Fehlschlag zeigte
sich höchstens als knapper Text in der Sync-Statusleiste.

EventApplier fängt jetzt jede Ausnahme pro Ereignis ab (geloggt über
AppLogger, übersprungen statt den Batch zu blockieren); SyncEngine
loggt jeden Sync-Fehlschlag vollständig. Betrifft nur den Desktop-Client,
kein API-Redeploy nötig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 21:53:30 +02:00
co-authored by Claude Sonnet 5
parent dc21cb2319
commit 722b7f87ff
5 changed files with 164 additions and 8 deletions
+24 -3
View File
@@ -1,6 +1,7 @@
using System.Text;
using JsonSerializer = System.Text.Json.JsonSerializer;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Data;
using LehrerApp.Sync.Crypto;
using LehrerApp.Sync.Models;
@@ -23,13 +24,18 @@ namespace LehrerApp.Sync;
/// Pfad bewusst NICHT geprüft (v1-Einschränkung, siehe TODO.md 10.3) — nur harte LiteDB-Unique-
/// Constraints greifen noch und führen zum Überspringen des einzelnen Ereignisses.
/// </summary>
public class EventApplier(LiteDbContext db, byte[] syncKey, HttpClient? http = null)
public class EventApplier(LiteDbContext db, byte[] syncKey, HttpClient? http = null, AppLogger? logger = null)
{
private static readonly Dictionary<string, EntityHandler> Handlers = BuildHandlers();
public async Task ApplyAsync(SyncEvent evt)
{
if (!Handlers.TryGetValue(evt.EntityType, out var handler)) return;
if (!Handlers.TryGetValue(evt.EntityType, out var handler))
{
logger?.Warn($"Sync: kein Handler für Entitätstyp '{evt.EntityType}' (EntityId={evt.EntityId}, " +
$"Operation={evt.Operation}) - Ereignis übersprungen.");
return;
}
try
{
var json = evt.Payload.Length == 0 ? "" : Decrypt(evt.Payload);
@@ -37,10 +43,25 @@ public class EventApplier(LiteDbContext db, byte[] syncKey, HttpClient? http = n
if (evt.EntityType == nameof(Documentation) && evt.Operation != "Delete" && http is not null)
await DownloadMissingAttachmentsAsync(json);
}
catch (LiteException)
catch (LiteException ex)
{
// Harte Constraint-Verletzung (z.B. Unique-Index) - dieses eine Ereignis
// überspringen, statt den gesamten Sync-Lauf abzubrechen.
logger?.Error($"Sync: Ereignis übersprungen (LiteDB-Constraint) - {evt.EntityType} " +
$"{evt.Operation} EntityId={evt.EntityId}", ex);
}
catch (Exception ex)
{
// Jede andere Ausnahme (z.B. Entschlüsselung/Deserialisierung fehlgeschlagen) darf
// NICHT aus ApplyAsync herausfallen: SyncEngine.PullAsync ruft dies in einer Schleife
// über einen ganzen Ereignis-Batch auf, ohne eigenes try/catch - eine unbehandelte
// Ausnahme hier würde den kompletten Pull-Lauf abbrechen, BEVOR SetLastServerSeq
// aufgerufen wird. Der Pull würde beim nächsten Versuch denselben Batch erneut laden
// und an genau demselben Ereignis wieder scheitern - ein dauerhaft blockierter Sync,
// bei dem selbst bereits erfolgreich angewendete Ereignisse im selben Batch nie als
// erledigt markiert werden.
logger?.Error($"Sync: Ereignis konnte nicht angewendet werden - {evt.EntityType} " +
$"{evt.Operation} EntityId={evt.EntityId}", ex);
}
}