diff --git a/reference-app/src/foregroundSyncMain/kotlin/dev/ohs/player/reference/app/data/sync/Sync.kt b/reference-app/src/foregroundSyncMain/kotlin/dev/ohs/player/reference/app/data/sync/Sync.kt index caff42a5..032283e1 100644 --- a/reference-app/src/foregroundSyncMain/kotlin/dev/ohs/player/reference/app/data/sync/Sync.kt +++ b/reference-app/src/foregroundSyncMain/kotlin/dev/ohs/player/reference/app/data/sync/Sync.kt @@ -160,30 +160,25 @@ internal object Sync { repeatInterval: Duration, retryConfiguration: RetryConfiguration?, ) { - mutex - .withLock { activePeriodicJobs[uniqueWorkName] } - ?.takeIf { it.isActive } - ?.let { - return - } - - val job = - scope.launch { - while (true) { - if (isNetworkConnected()) { - runAttemptsWithRetry( - taskFactory, - uniqueWorkName, - retryConfiguration, - syncTimeout = null, - ) {} - } else { - Logger.d { "Periodic sync cycle skipped for $uniqueWorkName — offline" } + mutex.withLock { + if (activePeriodicJobs[uniqueWorkName]?.isActive == true) return + activePeriodicJobs[uniqueWorkName] = + scope.launch { + while (true) { + if (isNetworkConnected()) { + runAttemptsWithRetry( + taskFactory, + uniqueWorkName, + retryConfiguration, + syncTimeout = null, + ) {} + } else { + Logger.d { "Periodic sync cycle skipped for $uniqueWorkName — offline" } + } + delay(repeatInterval) } - delay(repeatInterval) } - } - mutex.withLock { activePeriodicJobs[uniqueWorkName] = job } + } } suspend fun cancelSync(uniqueWorkName: String) { diff --git a/reference-app/src/jvmTest/kotlin/dev/ohs/player/reference/app/data/sync/SyncTest.kt b/reference-app/src/jvmTest/kotlin/dev/ohs/player/reference/app/data/sync/SyncTest.kt index b114ebf0..d293596f 100644 --- a/reference-app/src/jvmTest/kotlin/dev/ohs/player/reference/app/data/sync/SyncTest.kt +++ b/reference-app/src/jvmTest/kotlin/dev/ohs/player/reference/app/data/sync/SyncTest.kt @@ -37,7 +37,12 @@ import kotlin.test.assertIs import kotlin.test.assertTrue import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest /** @@ -138,23 +143,26 @@ class SyncTest { } @Test - fun periodicSync_calledTwice_doesNotStartASecondCompetingLoop() = runTest { - var startCount = 0 - val firstAttemptStarted = CompletableDeferred() - val factory: () -> TestFhirSyncTask = { - startCount++ - firstAttemptStarted.complete(Unit) - TestFhirSyncTask() + fun simultaneousPeriodicStartsRunOneLoop() = runBlocking { + var cycles = 0 + try { + List(20) { + async(Dispatchers.Default) { + Sync.periodicSync( + taskFactory = { + cycles++ + TestFhirSyncTask(CompletableDeferred()) + }, + repeatInterval = 1.minutes, + ) + } + } + .awaitAll() + delay(2_000) + } finally { + Sync.cancelPeriodicSync() } - Sync.periodicSync(taskFactory = factory, repeatInterval = 1.minutes) - firstAttemptStarted.await() - // The first cycle's loop is now sleeping the 1-minute interval, so it's still "active" — - // this second call must see that and no-op rather than starting a competing loop. - Sync.periodicSync(taskFactory = factory, repeatInterval = 1.minutes) - - assertEquals(1, startCount) - - Sync.cancelPeriodicSync() + assertEquals(1, cycles) } }