From b65872d88c156ea092802a9d96b5d4ec22a56f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Tue, 21 Jul 2026 11:11:56 +0200 Subject: [PATCH] Improve SSE polling reliability --- src/LabelPrintAgent/Backend/BackendClient.cs | 34 +++++++++++++++++-- .../Backend/BackendPollingWorker.cs | 9 +++-- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/LabelPrintAgent/Backend/BackendClient.cs b/src/LabelPrintAgent/Backend/BackendClient.cs index 0b01d7b..161ca89 100644 --- a/src/LabelPrintAgent/Backend/BackendClient.cs +++ b/src/LabelPrintAgent/Backend/BackendClient.cs @@ -138,6 +138,11 @@ public sealed class BackendClient settings.Backend.AgentId); } + // Das Backend sendet alle 30 Sekunden ein ": ping"-Keepalive. Kommt deutlich + // länger nichts an, ist die Verbindung tot (z. B. halboffenes TCP nach Standby, + // WLAN-Wechsel oder Server-Neustart) und muss neu aufgebaut werden. + private static readonly TimeSpan SseReadTimeout = TimeSpan.FromSeconds(90); + public async Task WatchServerSentEventsAsync(Func onLabelJobAvailable, CancellationToken cancellationToken = default) { var settings = _settingsStore.Load(); @@ -149,10 +154,33 @@ public sealed class BackendClient await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream); - while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested) + + // Nachholen: Jobs abrufen, die entstanden sind, während keine SSE-Verbindung bestand. + await onLabelJobAvailable(cancellationToken); + + using var watchdog = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + while (!cancellationToken.IsCancellationRequested) { - var line = await reader.ReadLineAsync(cancellationToken); - if (line is null || !line.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + watchdog.CancelAfter(SseReadTimeout); + + string? line; + try + { + line = await reader.ReadLineAsync(watchdog.Token); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + throw new TimeoutException( + $"Seit {SseReadTimeout.TotalSeconds:F0} Sekunden keine Daten vom SSE-Stream empfangen – Verbindung gilt als tot."); + } + + if (line is null) + { + // Server hat den Stream geschlossen; Aufrufer baut die Verbindung neu auf. + return; + } + + if (!line.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/src/LabelPrintAgent/Backend/BackendPollingWorker.cs b/src/LabelPrintAgent/Backend/BackendPollingWorker.cs index 12b7f8d..930a26f 100644 --- a/src/LabelPrintAgent/Backend/BackendPollingWorker.cs +++ b/src/LabelPrintAgent/Backend/BackendPollingWorker.cs @@ -40,10 +40,9 @@ public sealed class BackendPollingWorker : IDisposable public async Task PollOnceAsync(CancellationToken cancellationToken = default) { - if (!await _semaphore.WaitAsync(0, cancellationToken)) - { - return; - } + // Auf einen laufenden Poll warten statt abzubrechen: ein SSE-Trigger, der + // während eines Timer-Polls eintrifft, darf nicht verloren gehen. + await _semaphore.WaitAsync(cancellationToken); try { @@ -178,7 +177,7 @@ public sealed class BackendPollingWorker : IDisposable SetStatus(LastStatus.IsHealthy, "SSE-Verbindung wird aufgebaut."); await _backendClient.WatchServerSentEventsAsync(async token => { - SetStatus(true, "SSE: neuer Druckjob gemeldet."); + SetStatus(true, "SSE: Druckaufträge werden abgerufen."); await PollOnceAsync(token); }, cancellationToken); }