From 75cfc41924a0f10d02253bf23418dfbd974b501a Mon Sep 17 00:00:00 2001 From: Jens Reinemann Date: Sun, 17 May 2026 03:00:51 +0200 Subject: [PATCH] fix(websocket): Reconnect-Strategie robuster machen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebSocketClientImpl: - Jitter (±25%) zum exponentiellen Backoff hinzugefügt, um Thundering-Herd-Effekt bei Server-Neustart zu vermeiden - Retry-Zähler mit MAX_RETRIES=5: nach 5 konsekutiven Fehlschlägen wird ConnectionFailed-Event emittiert (nach ~62s) - Client versucht weiterhin im MAX_BACKOFF-Intervall (60s) zu reconnecten, gibt nicht vollständig auf - Zähler wird bei erfolgreicher Verbindung zurückgesetzt WebSocketEvent: neues ConnectionFailed(message) Event hinzugefügt SettingsViewModel: ConnectionFailed -> SyncStatus.Error, Connected -> SyncStatus.Idle (Fehler wird beim Reconnect gelöscht) Closes #73 --- .../app/data/sync/WebSocketClient.kt | 1 + .../app/data/sync/WebSocketClientImpl.kt | 20 +++++++++++++++++-- .../app/ui/settings/SettingsViewModel.kt | 6 ++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClient.kt b/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClient.kt index 72dcd25..c26229e 100644 --- a/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClient.kt +++ b/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClient.kt @@ -14,5 +14,6 @@ internal sealed interface WebSocketEvent { data object FullSyncRequired : WebSocketEvent data object Connected : WebSocketEvent data object Disconnected : WebSocketEvent + data class ConnectionFailed(val message: String) : WebSocketEvent data class NewMessage(val message: MessageDto) : WebSocketEvent } diff --git a/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClientImpl.kt b/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClientImpl.kt index b0b4c74..d845a8b 100644 --- a/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClientImpl.kt +++ b/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClientImpl.kt @@ -22,6 +22,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import javax.inject.Inject import javax.inject.Singleton +import kotlin.random.Random @Singleton internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { @@ -40,6 +41,8 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { connectionJob?.cancel() connectionJob = scope.launch { var backoffMs = INITIAL_BACKOFF_MS + var consecutiveFailures = 0 + var connectionFailedEmitted = false while (isActive) { try { val wsUrl = serverUrl.trimEnd('/') @@ -47,6 +50,8 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { .replace("http://", "ws://") wsHttpClient.webSocket("$wsUrl/ws/sync?token=$accessToken") { backoffMs = INITIAL_BACKOFF_MS + consecutiveFailures = 0 + connectionFailedEmitted = false _events.emit(WebSocketEvent.Connected) for (frame in incoming) { if (frame is Frame.Text) { @@ -57,11 +62,20 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { } catch (e: CancellationException) { break } catch (_: Exception) { - // connection failed – retry after backoff + consecutiveFailures++ + if (consecutiveFailures >= MAX_RETRIES && !connectionFailedEmitted) { + _events.emit( + WebSocketEvent.ConnectionFailed( + "Verbindung nach $MAX_RETRIES Versuchen fehlgeschlagen" + ) + ) + connectionFailedEmitted = true + } } if (!isActive) break _events.emit(WebSocketEvent.Disconnected) - delay(backoffMs) + val jitter = backoffMs * JITTER_FACTOR * (Random.nextDouble() * 2 - 1) + delay(backoffMs + jitter.toLong()) backoffMs = minOf(backoffMs * 2, MAX_BACKOFF_MS) } } @@ -100,6 +114,8 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { private companion object { const val INITIAL_BACKOFF_MS = 2_000L const val MAX_BACKOFF_MS = 60_000L + const val MAX_RETRIES = 5 + const val JITTER_FACTOR = 0.25 val json = Json { ignoreUnknownKeys = true } } } diff --git a/app/src/main/java/de/krisenvorrat/app/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/krisenvorrat/app/ui/settings/SettingsViewModel.kt index ab0017f..9e129e3 100644 --- a/app/src/main/java/de/krisenvorrat/app/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/krisenvorrat/app/ui/settings/SettingsViewModel.kt @@ -53,6 +53,12 @@ internal class SettingsViewModel @Inject constructor( when (event) { is WebSocketEvent.FullSyncRequired -> pullSync() is WebSocketEvent.InventoryUpdated -> pullSync() + is WebSocketEvent.ConnectionFailed -> { + _uiState.update { it.copy(syncStatus = SyncStatus.Error(event.message)) } + } + is WebSocketEvent.Connected -> { + _uiState.update { it.copy(syncStatus = SyncStatus.Idle) } + } else -> {} } }