From 25505d6e8aaeb1e5dddd3e32e5b4afe28317126b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 15 Sep 2026 10:00:43 +0200 Subject: [PATCH] Do not automatically restart the sync in case it is Terminated. Also simplify the computation of the backoff delay. --- .../appnav/session/SyncOrchestrator.kt | 25 ++++++++++--------- .../android/appnav/SyncOrchestratorTest.kt | 25 +++---------------- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/session/SyncOrchestrator.kt b/appnav/src/main/kotlin/io/element/android/appnav/session/SyncOrchestrator.kt index 786365b7be..9863f75410 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/session/SyncOrchestrator.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/session/SyncOrchestrator.kt @@ -138,7 +138,7 @@ class SyncOrchestrator( SyncStateAction.StopSync } else if (syncState == SyncState.Idle && isAppActive && isNetworkAvailable) { SyncStateAction.StartSync - } else if (syncState.isDead() && isAppActive && isNetworkAvailable) { + } else if (syncState == SyncState.Error && isAppActive && isNetworkAvailable) { // The sync service has given up, for instance because the homeserver has expired our sliding sync // session and answered 400 M_UNKNOWN_POS. Nothing else will ever restart it, so do it from here, // else the application is stuck until its data is cleared. @@ -184,8 +184,18 @@ class SyncOrchestrator( @VisibleForTesting internal val FIRST_RESTART_DELAY = 1.seconds +/** + * Exponential backoff, from [FIRST_RESTART_DELAY], doubling on every attempt, up to [FIRST_RESTART_DELAY * 32]. + */ @VisibleForTesting -internal val MAX_RESTART_DELAY = 30.seconds +internal val RESTART_BACKOFF_DELAYS = listOf( + FIRST_RESTART_DELAY, + FIRST_RESTART_DELAY * 2, + FIRST_RESTART_DELAY * 4, + FIRST_RESTART_DELAY * 8, + FIRST_RESTART_DELAY * 16, + FIRST_RESTART_DELAY * 32, +) /** * Duration during which the sync service has to stay running before the restart backoff is reset. @@ -193,17 +203,8 @@ internal val MAX_RESTART_DELAY = 30.seconds @VisibleForTesting internal val RESTART_BACKOFF_RESET_DELAY = 30.seconds -/** - * Return true if the sync service has stopped and will not restart by itself. - */ -private fun SyncState.isDead() = this == SyncState.Error || this == SyncState.Terminated - -/** - * Exponential backoff, from [FIRST_RESTART_DELAY], doubling on every attempt, up to [MAX_RESTART_DELAY]. - */ private fun restartDelay(attempt: Int): Duration { - val exponent = attempt.coerceIn(0, 5) - return (FIRST_RESTART_DELAY * (1 shl exponent)).coerceAtMost(MAX_RESTART_DELAY) + return RESTART_BACKOFF_DELAYS[attempt.coerceIn(0, RESTART_BACKOFF_DELAYS.lastIndex)] } private enum class SyncStateAction { diff --git a/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt index 9475399a85..af53c68b3d 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt @@ -9,7 +9,7 @@ package io.element.android.appnav import io.element.android.appnav.session.FIRST_RESTART_DELAY -import io.element.android.appnav.session.MAX_RESTART_DELAY +import io.element.android.appnav.session.RESTART_BACKOFF_DELAYS import io.element.android.appnav.session.RESTART_BACKOFF_RESET_DELAY import io.element.android.appnav.session.SyncOrchestrator import io.element.android.features.networkmonitor.api.NetworkStatus @@ -441,25 +441,6 @@ class SyncOrchestratorTest { startSyncRecorder.assertions().isCalledOnce() } - @Test - fun `when the sync service is terminated, it will be restarted after a delay`() = runTest { - val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } - val syncService = FakeSyncService(initialSyncState = SyncState.Terminated).apply { - startSyncLambda = startSyncRecorder - } - val syncOrchestrator = createSyncOrchestrator( - matrixClient = FakeMatrixClient(syncService = syncService), - networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected), - appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true), - ) - - // We start observing - syncOrchestrator.observeStates() - - advanceTimeBy(FIRST_RESTART_DELAY * 2) - startSyncRecorder.assertions().isCalledOnce() - } - @Test fun `when the sync service keeps dying, the delay between the restarts increases`() = runTest { val startSyncRecorder = lambdaRecorder> { Result.success(Unit) } @@ -538,7 +519,7 @@ class SyncOrchestratorTest { // We start observing syncOrchestrator.observeStates() - advanceTimeBy(MAX_RESTART_DELAY * 2) + advanceTimeBy(RESTART_BACKOFF_DELAYS.last() * 2) startSyncRecorder.assertions().isNeverCalled() } @@ -557,7 +538,7 @@ class SyncOrchestratorTest { // We start observing syncOrchestrator.observeStates() - advanceTimeBy(MAX_RESTART_DELAY * 2) + advanceTimeBy(RESTART_BACKOFF_DELAYS.last() * 2) startSyncRecorder.assertions().isNeverCalled() }