Skip to content

Commit d1fc963

Browse files
authored
SyncOrchestrator: restore the initial sync step (#4242)
* SyncOrchestrator: restore the initial sync step * Try having internal and public functions to be able to unit test the initial sync and the state changes separately, as well as the initial sync followed by a state change * Only manually start sync if the `SyncService` was previously stopped, don't do it for `Offline` state
1 parent dc14992 commit d1fc963

File tree

2 files changed

+75
-16
lines changed

2 files changed

+75
-16
lines changed

appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package io.element.android.appnav.di
99

10+
import androidx.annotation.VisibleForTesting
1011
import dagger.assisted.Assisted
1112
import dagger.assisted.AssistedFactory
1213
import dagger.assisted.AssistedInject
@@ -21,9 +22,9 @@ import kotlinx.coroutines.FlowPreview
2122
import kotlinx.coroutines.flow.combine
2223
import kotlinx.coroutines.flow.debounce
2324
import kotlinx.coroutines.flow.distinctUntilChanged
24-
import kotlinx.coroutines.flow.launchIn
25+
import kotlinx.coroutines.flow.first
2526
import kotlinx.coroutines.flow.onCompletion
26-
import kotlinx.coroutines.flow.onEach
27+
import kotlinx.coroutines.launch
2728
import timber.log.Timber
2829
import java.util.concurrent.atomic.AtomicBoolean
2930
import kotlin.time.Duration.Companion.milliseconds
@@ -53,13 +54,28 @@ class SyncOrchestrator @AssistedInject constructor(
5354
*
5455
* Before observing the state, a first attempt at starting the sync service will happen if it's not already running.
5556
*/
56-
@OptIn(FlowPreview::class)
5757
fun start() {
5858
if (!started.compareAndSet(false, true)) {
5959
Timber.tag(tag).d("already started, exiting early")
6060
return
6161
}
6262

63+
coroutineScope.launch {
64+
// Perform an initial sync if the sync service is not running, to check whether the homeserver is accessible
65+
// Otherwise, if the device is offline the sync service will never start and the SyncState will be Idle, not Offline
66+
Timber.tag(tag).d("performing initial sync attempt")
67+
syncService.startSync()
68+
69+
// Wait until the sync service is not idle, either it will be running or in error/offline state
70+
syncService.syncState.first { it != SyncState.Idle }
71+
72+
observeStates()
73+
}
74+
}
75+
76+
@OptIn(FlowPreview::class)
77+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
78+
internal fun observeStates() = coroutineScope.launch {
6379
Timber.tag(tag).d("start observing the app and network state")
6480

6581
combine(
@@ -76,7 +92,7 @@ class SyncOrchestrator @AssistedInject constructor(
7692
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
7793
if (syncState == SyncState.Running && !isAppActive) {
7894
SyncStateAction.StopSync
79-
} else if (syncState != SyncState.Running && isAppActive && isNetworkAvailable) {
95+
} else if (syncState == SyncState.Idle && isAppActive && isNetworkAvailable) {
8096
SyncStateAction.StartSync
8197
} else {
8298
SyncStateAction.NoOp
@@ -87,7 +103,10 @@ class SyncOrchestrator @AssistedInject constructor(
87103
// Don't stop the sync immediately, wait a bit to avoid starting/stopping the sync too often
88104
if (action == SyncStateAction.StopSync) 3.seconds else 0.seconds
89105
}
90-
.onEach { action ->
106+
.onCompletion {
107+
Timber.tag(tag).d("has been stopped")
108+
}
109+
.collect { action ->
91110
when (action) {
92111
SyncStateAction.StartSync -> {
93112
syncService.startSync()
@@ -98,10 +117,6 @@ class SyncOrchestrator @AssistedInject constructor(
98117
SyncStateAction.NoOp -> Unit
99118
}
100119
}
101-
.onCompletion {
102-
Timber.tag(tag).d("has been stopped")
103-
}
104-
.launchIn(coroutineScope)
105120
}
106121
}
107122

appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,50 @@ class SyncOrchestratorTest {
3131
@get:Rule
3232
val warmUpRule = WarmUpRule()
3333

34+
@Test
35+
fun `when the sync wasn't running before, an initial sync will take place, even with no network`() = runTest {
36+
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
37+
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
38+
startSyncLambda = startSyncRecorder
39+
}
40+
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
41+
val syncOrchestrator = createSyncOrchestrator(
42+
syncService = syncService,
43+
networkMonitor = networkMonitor,
44+
)
45+
46+
// We start observing with an initial sync
47+
syncOrchestrator.start()
48+
49+
// Advance the time just enough to make sure the initial sync has run
50+
advanceTimeBy(1.milliseconds)
51+
startSyncRecorder.assertions().isCalledOnce()
52+
}
53+
54+
@Test
55+
fun `when the sync wasn't running before, an initial sync will take place`() = runTest {
56+
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
57+
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
58+
startSyncLambda = startSyncRecorder
59+
}
60+
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
61+
val syncOrchestrator = createSyncOrchestrator(
62+
syncService = syncService,
63+
networkMonitor = networkMonitor,
64+
)
65+
66+
// We start observing with an initial sync
67+
syncOrchestrator.start()
68+
69+
// Advance the time just enough to make sure the initial sync has run
70+
advanceTimeBy(1.milliseconds)
71+
startSyncRecorder.assertions().isCalledOnce()
72+
73+
// If we wait for a while, the sync will not be started again by the observer since it's already running
74+
advanceTimeBy(10.seconds)
75+
startSyncRecorder.assertions().isCalledOnce()
76+
}
77+
3478
@Test
3579
fun `when the app goes to background and the sync was running, it will be stopped after a delay`() = runTest {
3680
val stopSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
@@ -46,7 +90,7 @@ class SyncOrchestratorTest {
4690
)
4791

4892
// We start observing
49-
syncOrchestrator.start()
93+
syncOrchestrator.observeStates()
5094

5195
// Advance the time to make sure the orchestrator has had time to start processing the inputs
5296
advanceTimeBy(100.milliseconds)
@@ -78,7 +122,7 @@ class SyncOrchestratorTest {
78122
)
79123

80124
// We start observing
81-
syncOrchestrator.start()
125+
syncOrchestrator.observeStates()
82126

83127
// Advance the time to make sure the orchestrator has had time to start processing the inputs
84128
advanceTimeBy(100.milliseconds)
@@ -126,7 +170,7 @@ class SyncOrchestratorTest {
126170
)
127171

128172
// We start observing
129-
syncOrchestrator.start()
173+
syncOrchestrator.observeStates()
130174

131175
// Advance the time to make sure the orchestrator has had time to start processing the inputs
132176
advanceTimeBy(100.milliseconds)
@@ -169,7 +213,7 @@ class SyncOrchestratorTest {
169213
)
170214

171215
// We start observing
172-
syncOrchestrator.start()
216+
syncOrchestrator.observeStates()
173217

174218
// Advance the time to make sure the orchestrator has had time to start processing the inputs
175219
advanceTimeBy(100.milliseconds)
@@ -213,7 +257,7 @@ class SyncOrchestratorTest {
213257
)
214258

215259
// We start observing
216-
syncOrchestrator.start()
260+
syncOrchestrator.observeStates()
217261

218262
// Advance the time to make sure the orchestrator has had time to start processing the inputs
219263
advanceTimeBy(100.milliseconds)
@@ -256,7 +300,7 @@ class SyncOrchestratorTest {
256300
)
257301

258302
// We start observing
259-
syncOrchestrator.start()
303+
syncOrchestrator.observeStates()
260304

261305
// Advance the time to make sure the orchestrator has had time to start processing the inputs
262306
advanceTimeBy(100.milliseconds)
@@ -285,7 +329,7 @@ class SyncOrchestratorTest {
285329
)
286330

287331
// We start observing
288-
syncOrchestrator.start()
332+
syncOrchestrator.observeStates()
289333

290334
// This should still not trigger a sync, since there is no network
291335
advanceTimeBy(10.seconds)

0 commit comments

Comments
 (0)