Do not automatically restart the sync in case it is Terminated.

Also simplify the computation of the backoff delay.
This commit is contained in:
Benoit Marty
2026-09-15 10:00:43 +02:00
committed by Benoit Marty
parent b28b2ceeed
commit 25505d6e8a
2 changed files with 16 additions and 34 deletions
@@ -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 {
@@ -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<Unit>> { 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<Unit>> { 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()
}