Improve SSE polling reliability

This commit is contained in:
2026-07-21 11:11:56 +02:00
parent cfe141efc9
commit b65872d88c
2 changed files with 35 additions and 8 deletions
+31 -3
View File
@@ -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<CancellationToken, Task> 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;
}
@@ -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);
}