fix(websocket): Reconnect-Strategie robuster machen
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
This commit is contained in:
parent
eb5bdd4b7b
commit
75cfc41924
3 changed files with 25 additions and 2 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue