Update Sync
This commit is contained in:
@@ -49,8 +49,14 @@ public class EventQueue : IDisposable
|
||||
return evt;
|
||||
}
|
||||
|
||||
public List<SyncEvent> GetPending(int max = 200) =>
|
||||
_queue.Find(Query.All(nameof(SyncEvent.SequenceNr))).Take(max).ToList();
|
||||
public List<SyncEvent> GetPending(int max = SyncProtocol.PushBatchSize,
|
||||
IReadOnlySet<Guid>? excludedEventIds = null)
|
||||
{
|
||||
var pending = _queue.Find(Query.All(nameof(SyncEvent.SequenceNr))).AsEnumerable();
|
||||
if (excludedEventIds is not null)
|
||||
pending = pending.Where(e => !excludedEventIds.Contains(e.EventId));
|
||||
return pending.Take(max).ToList();
|
||||
}
|
||||
public int PendingCount() => _queue.Count();
|
||||
public void Acknowledge(IEnumerable<Guid> ids) { foreach (var id in ids) _queue.Delete(id); }
|
||||
public long GetLastServerSeq() => _meta.FindById("serverSeq")?.Value ?? 0;
|
||||
|
||||
@@ -86,14 +86,37 @@ public class SyncEngine : IDisposable
|
||||
|
||||
private async Task<SyncResult> RunSyncAsync(bool isAutomatic)
|
||||
{
|
||||
var pulledDataChanged = false;
|
||||
SetState(SyncState.Syncing);
|
||||
_logger?.Info($"Sync: Start ({(isAutomatic ? "automatisch" : "manuell")}), Gerät={_config.DeviceId}, " +
|
||||
$"{_queue.PendingCount()} lokal ausstehend");
|
||||
try
|
||||
{
|
||||
var (pushed, pushConflicts) = await PushAsync();
|
||||
var pushed = 0;
|
||||
var pushConflicts = 0;
|
||||
var deferredPushEvents = new HashSet<Guid>();
|
||||
PushBatchResult pushBatch;
|
||||
do
|
||||
{
|
||||
pushBatch = await PushAsync(deferredPushEvents);
|
||||
pushed += pushBatch.Pushed;
|
||||
pushConflicts += pushBatch.Conflicts;
|
||||
deferredPushEvents.UnionWith(pushBatch.DeferredEventIds);
|
||||
} while (pushBatch.SourceEventCount == SyncProtocol.PushBatchSize);
|
||||
|
||||
await _attachments.UploadPendingAsync(_queue);
|
||||
var (pulled, conflicts) = await PullAsync();
|
||||
|
||||
var pulled = 0;
|
||||
var conflicts = 0;
|
||||
PullBatchResult pullBatch;
|
||||
do
|
||||
{
|
||||
pullBatch = await PullAsync();
|
||||
pulled += pullBatch.Pulled;
|
||||
conflicts += pullBatch.Conflicts;
|
||||
pulledDataChanged |= pullBatch.Pulled > 0;
|
||||
} while (pullBatch.Pulled == SyncProtocol.PullBatchSize);
|
||||
|
||||
_queue.SetLastSyncAt(DateTime.UtcNow);
|
||||
SetState(SyncState.Idle);
|
||||
_logger?.Info($"Sync: Fertig - {pushed} gepusht ({pushConflicts} Push-Konflikte), " +
|
||||
@@ -121,12 +144,19 @@ public class SyncEngine : IDisposable
|
||||
SetState(SyncState.Error, ex.Message);
|
||||
return new() { Reason = ex.Message };
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Auch wenn ein späterer Batch fehlschlägt, wurden frühere Batches bereits dauerhaft
|
||||
// angewendet. Die Oberfläche muss diesen erfolgreich übernommenen Zwischenstand dann
|
||||
// trotzdem neu laden; bei einem erfolgreichen Drain feuert der Hook genau einmal.
|
||||
if (pulledDataChanged) DataChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(int Pushed, int Conflicts)> PushAsync()
|
||||
private async Task<PushBatchResult> PushAsync(IReadOnlySet<Guid> excludedEventIds)
|
||||
{
|
||||
var pending = DeduplicatePending();
|
||||
if (pending.Count == 0) return (0, 0);
|
||||
var pending = DeduplicatePending(excludedEventIds, out var sourceEventCount);
|
||||
if (pending.Count == 0) return new(0, 0, sourceEventCount, []);
|
||||
// BasedOnServerSeq erst unmittelbar vor dem Senden setzen (nicht beim Enqueue) - zwischen
|
||||
// Enqueue und Push kann ein Pull den lokal bekannten Stand dieser Entität aktualisiert
|
||||
// haben (siehe EventApplier.ApplyAsync).
|
||||
@@ -139,7 +169,11 @@ public class SyncEngine : IDisposable
|
||||
using var resp = await _http.SendAsync(request);
|
||||
await SyncProtocol.EnsureCompatibleSuccessAsync(resp);
|
||||
var result = await resp.Content.ReadFromJsonAsync<PushResponse>();
|
||||
if (result is null) { _logger?.Warn("Sync: Push - leere Server-Antwort."); return (0, 0); }
|
||||
if (result is null)
|
||||
{
|
||||
_logger?.Warn("Sync: Push - leere Server-Antwort.");
|
||||
return new(0, 0, sourceEventCount, pending.Select(e => e.EventId).ToList());
|
||||
}
|
||||
_queue.Acknowledge(pending
|
||||
.Where(e => !result.ConflictingEventIds.Contains(e.EventId))
|
||||
.Select(e => e.EventId));
|
||||
@@ -161,8 +195,8 @@ public class SyncEngine : IDisposable
|
||||
$"{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,
|
||||
result.ConflictingEventIds.Count);
|
||||
return new(pending.Count - result.ConflictingEventIds.Count,
|
||||
result.ConflictingEventIds.Count, sourceEventCount, result.ConflictingEventIds);
|
||||
}
|
||||
|
||||
// Payload ist immer ein vollständiges Entitäts-Snapshot (nie ein Delta, siehe
|
||||
@@ -171,9 +205,11 @@ public class SyncEngine : IDisposable
|
||||
// exakte BasedOnServerSeq-Prüfung: ohne Dedup könnten zwei Ereignisse derselben Entität im
|
||||
// selben Batch mit demselben (veralteten) BasedOnServerSeq ankommen und sich gegenseitig ins
|
||||
// Aus laufen.
|
||||
private List<SyncEvent> DeduplicatePending()
|
||||
private List<SyncEvent> DeduplicatePending(IReadOnlySet<Guid> excludedEventIds,
|
||||
out int sourceEventCount)
|
||||
{
|
||||
var pending = _queue.GetPending();
|
||||
var pending = _queue.GetPending(SyncProtocol.PushBatchSize, excludedEventIds);
|
||||
sourceEventCount = pending.Count;
|
||||
if (pending.Count == 0) return pending;
|
||||
var latest = pending
|
||||
.GroupBy(e => (e.EntityType, e.EntityId))
|
||||
@@ -259,7 +295,7 @@ public class SyncEngine : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(int Pulled, int Conflicts)> PullAsync()
|
||||
private async Task<PullBatchResult> PullAsync()
|
||||
{
|
||||
var since = _queue.GetLastServerSeq();
|
||||
_logger?.Info($"Sync: Pull - frage Server nach Ereignissen seit ServerSequenceNr={since}.");
|
||||
@@ -271,7 +307,7 @@ public class SyncEngine : IDisposable
|
||||
if (resp is null || resp.Events.Count == 0)
|
||||
{
|
||||
_logger?.Info("Sync: Pull - keine neuen Ereignisse vom Server.");
|
||||
return (0, 0);
|
||||
return new(0, 0);
|
||||
}
|
||||
_logger?.Info($"Sync: Pull - {resp.Events.Count} Ereignis(se) vom Server erhalten: " +
|
||||
string.Join(", ", resp.Events.Select(e => $"{e.EntityType}/{e.Operation}")));
|
||||
@@ -287,10 +323,13 @@ public class SyncEngine : IDisposable
|
||||
if (c.Resolution == "RemoteWon") await _applier.ApplyAsync(evt);
|
||||
}
|
||||
_queue.SetLastServerSeq(resp.ServerSequenceNr);
|
||||
DataChanged?.Invoke();
|
||||
return (resp.Events.Count, conflicts);
|
||||
return new(resp.Events.Count, conflicts);
|
||||
}
|
||||
|
||||
private readonly record struct PushBatchResult(int Pushed, int Conflicts,
|
||||
int SourceEventCount, IReadOnlyCollection<Guid> DeferredEventIds);
|
||||
private readonly record struct PullBatchResult(int Pulled, int Conflicts);
|
||||
|
||||
private void SetState(SyncState state, string? error = null)
|
||||
{
|
||||
Status = new SyncStatus
|
||||
|
||||
@@ -14,6 +14,8 @@ public static class SyncProtocol
|
||||
{
|
||||
public const string CurrentVersion = "1";
|
||||
public const string VersionHeaderName = "X-LehrerApp-Sync-Version";
|
||||
public const int PushBatchSize = 200;
|
||||
public const int PullBatchSize = 500;
|
||||
|
||||
public static HttpRequestMessage CreateRequest(HttpMethod method, string requestUri)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user