From fbf2fb5f791cf9c3597e9302042b60380829db52 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 17 Feb 2026 11:46:12 +0200 Subject: [PATCH 1/3] MS-1350 Only apply initial delay when it is explicitly provided --- .../infra/sync/extensions/WorkManager.ext.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/extensions/WorkManager.ext.kt b/infra/sync/src/main/java/com/simprints/infra/sync/extensions/WorkManager.ext.kt index 655626e3c4..7a3b6db335 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/extensions/WorkManager.ext.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/extensions/WorkManager.ext.kt @@ -24,7 +24,7 @@ internal inline fun WorkManager.schedulePeriodicW workName: String, repeatInterval: Long, existingWorkPolicy: ExistingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE, - initialDelay: Long = 0, + initialDelay: Long = -1, backoffInterval: Long = SyncConstants.DEFAULT_BACKOFF_INTERVAL_MINUTES, constraints: Constraints = defaultWorkerConstraints(), tags: List = emptyList(), @@ -34,8 +34,13 @@ internal inline fun WorkManager.schedulePeriodicW existingWorkPolicy, PeriodicWorkRequestBuilder(repeatInterval, SyncConstants.SYNC_TIME_UNIT) .setConstraints(constraints) - .setInitialDelay(initialDelay, SyncConstants.SYNC_TIME_UNIT) - .setBackoffCriteria(BackoffPolicy.LINEAR, backoffInterval, SyncConstants.SYNC_TIME_UNIT) + .let { + if (initialDelay >= 0) { + it.setInitialDelay(initialDelay, SyncConstants.SYNC_TIME_UNIT) + } else { + it + } + }.setBackoffCriteria(BackoffPolicy.LINEAR, backoffInterval, SyncConstants.SYNC_TIME_UNIT) .let { if (inputData != null) it.setInputData(inputData) else it } .let { tags.fold(it) { builder, tag -> builder.addTag(tag) } } .build(), From 17096131ba7021bdc21286fa380de6f9dc3f13f9 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 17 Feb 2026 12:07:48 +0200 Subject: [PATCH 2/3] MS-1350 Do not run explicit unscheduling and rely on KEEP/UPDATE policy instead --- .../infra/sync/SyncOrchestratorImpl.kt | 1 - ...ResetLocalRecordsIfConfigChangedUseCase.kt | 11 ++--- ...tLocalRecordsIfConfigChangedUseCaseTest.kt | 44 +++++++------------ 3 files changed, 20 insertions(+), 36 deletions(-) diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt index e30688142a..888eab1453 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt @@ -223,7 +223,6 @@ internal class SyncOrchestratorImpl @Inject constructor( Job().apply { complete() } } ScheduleCommand.Action.RESCHEDULE -> { - unschedule() appScope.launch(ioDispatcher) { blockWhileUnscheduled?.invoke() reschedule() diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/ResetLocalRecordsIfConfigChangedUseCase.kt b/infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/ResetLocalRecordsIfConfigChangedUseCase.kt index 15a071ff41..2ca0537277 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/ResetLocalRecordsIfConfigChangedUseCase.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/ResetLocalRecordsIfConfigChangedUseCase.kt @@ -18,13 +18,10 @@ internal class ResetLocalRecordsIfConfigChangedUseCase @Inject constructor( newConfig: ProjectConfiguration, ) { if (hasPartitionTypeChanged(oldConfig, newConfig)) { - syncOrchestrator - .execute( - ScheduleCommand.Events.rescheduleAfter { - resetDownSyncInfo() - enrolmentRecordRepository.deleteAll() - }, - ).await() + syncOrchestrator.execute(ScheduleCommand.Events.unschedule()).await() + resetDownSyncInfo() + enrolmentRecordRepository.deleteAll() + syncOrchestrator.execute(ScheduleCommand.Events.reschedule()).await() } } diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/config/usecase/ResetLocalRecordsIfConfigChangedUseCaseTest.kt b/infra/sync/src/test/java/com/simprints/infra/sync/config/usecase/ResetLocalRecordsIfConfigChangedUseCaseTest.kt index e5dbab6482..fc5a929296 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/config/usecase/ResetLocalRecordsIfConfigChangedUseCaseTest.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/config/usecase/ResetLocalRecordsIfConfigChangedUseCaseTest.kt @@ -1,6 +1,5 @@ package com.simprints.infra.sync.config.usecase -import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.config.store.models.DownSynchronizationConfiguration import com.simprints.infra.config.store.models.Frequency @@ -98,13 +97,11 @@ class ResetLocalRecordsIfConfigChangedUseCaseTest { ), ) - verify { syncOrchestrator.execute(any()) } - val command = scheduleCommandSlot.captured as ScheduleCommand.EventsCommand - assertThat(command.action).isEqualTo(ScheduleCommand.Action.RESCHEDULE) - assertThat(command.withDelay).isFalse() - assertThat(command.blockWhileUnscheduled).isNotNull() + verify(exactly = 1) { + syncOrchestrator.execute(match { it.action == ScheduleCommand.Action.UNSCHEDULE }) + syncOrchestrator.execute(match { it.action == ScheduleCommand.Action.RESCHEDULE }) + } - command.blockWhileUnscheduled?.invoke() runCurrent() coVerify { resetDownSyncInfo() @@ -135,13 +132,10 @@ class ResetLocalRecordsIfConfigChangedUseCaseTest { ), ) - verify { syncOrchestrator.execute(any()) } - val command = scheduleCommandSlot.captured as ScheduleCommand.EventsCommand - assertThat(command.action).isEqualTo(ScheduleCommand.Action.RESCHEDULE) - assertThat(command.blockWhileUnscheduled).isNotNull() - - command.blockWhileUnscheduled?.invoke() - runCurrent() + verify(exactly = 1) { + syncOrchestrator.execute(match { it.action == ScheduleCommand.Action.UNSCHEDULE }) + syncOrchestrator.execute(match { it.action == ScheduleCommand.Action.RESCHEDULE }) + } coVerify { resetDownSyncInfo() enrolmentRecordRepository.deleteAll() @@ -171,13 +165,10 @@ class ResetLocalRecordsIfConfigChangedUseCaseTest { ), ) - verify { syncOrchestrator.execute(any()) } - val command = scheduleCommandSlot.captured as ScheduleCommand.EventsCommand - assertThat(command.action).isEqualTo(ScheduleCommand.Action.RESCHEDULE) - assertThat(command.blockWhileUnscheduled).isNotNull() - - command.blockWhileUnscheduled?.invoke() - runCurrent() + verify(exactly = 1) { + syncOrchestrator.execute(match { it.action == ScheduleCommand.Action.UNSCHEDULE }) + syncOrchestrator.execute(match { it.action == ScheduleCommand.Action.RESCHEDULE }) + } coVerify { resetDownSyncInfo() enrolmentRecordRepository.deleteAll() @@ -207,13 +198,10 @@ class ResetLocalRecordsIfConfigChangedUseCaseTest { ), ) - verify { syncOrchestrator.execute(any()) } - val command = scheduleCommandSlot.captured as ScheduleCommand.EventsCommand - assertThat(command.action).isEqualTo(ScheduleCommand.Action.RESCHEDULE) - assertThat(command.blockWhileUnscheduled).isNotNull() - - command.blockWhileUnscheduled?.invoke() - runCurrent() + verify(exactly = 1) { + syncOrchestrator.execute(match { it.action == ScheduleCommand.Action.UNSCHEDULE }) + syncOrchestrator.execute(match { it.action == ScheduleCommand.Action.RESCHEDULE }) + } coVerify { resetDownSyncInfo() enrolmentRecordRepository.deleteAll() From 1caa8d272594f9d2e3c639ab853c07ccfbac3d91 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 17 Feb 2026 12:23:33 +0200 Subject: [PATCH 3/3] MS-1350 Remove the redundant rescheduleAfter command in favour of explicit separate calls --- .../infra/sync/ScheduleSyncCommand.kt | 16 ----- .../infra/sync/SyncOrchestratorImpl.kt | 5 -- .../infra/sync/ScheduleSyncCommandTest.kt | 17 ------ .../SyncOrchestratorCommandExecutionTest.kt | 61 ------------------- 4 files changed, 99 deletions(-) diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/ScheduleSyncCommand.kt b/infra/sync/src/main/java/com/simprints/infra/sync/ScheduleSyncCommand.kt index abf37e2646..83aa579534 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/ScheduleSyncCommand.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/ScheduleSyncCommand.kt @@ -14,48 +14,32 @@ sealed class ScheduleCommand { internal data class EverythingCommand( val action: Action, val withDelay: Boolean = false, - val blockWhileUnscheduled: (suspend () -> Unit)? = null, ) : ScheduleCommand() internal data class EventsCommand( val action: Action, val withDelay: Boolean = false, - val blockWhileUnscheduled: (suspend () -> Unit)? = null, ) : ScheduleCommand() internal data class ImagesCommand( val action: Action, - val blockWhileUnscheduled: (suspend () -> Unit)? = null, ) : ScheduleCommand() object Everything { fun reschedule(withDelay: Boolean = false): ScheduleCommand = EverythingCommand(action = Action.RESCHEDULE, withDelay = withDelay) fun unschedule(): ScheduleCommand = EverythingCommand(action = Action.UNSCHEDULE) - - fun rescheduleAfter( - withDelay: Boolean = false, - block: suspend () -> Unit, - ): ScheduleCommand = EverythingCommand(action = Action.RESCHEDULE, withDelay = withDelay, blockWhileUnscheduled = block) } object Events { fun reschedule(withDelay: Boolean = false): ScheduleCommand = EventsCommand(action = Action.RESCHEDULE, withDelay = withDelay) fun unschedule(): ScheduleCommand = EventsCommand(action = Action.UNSCHEDULE) - - fun rescheduleAfter( - withDelay: Boolean = false, - block: suspend () -> Unit, - ): ScheduleCommand = EventsCommand(action = Action.RESCHEDULE, withDelay = withDelay, blockWhileUnscheduled = block) } object Images { fun reschedule(): ScheduleCommand = ImagesCommand(action = Action.RESCHEDULE) fun unschedule(): ScheduleCommand = ImagesCommand(action = Action.UNSCHEDULE) - - fun rescheduleAfter(block: suspend () -> Unit): ScheduleCommand = - ImagesCommand(action = Action.RESCHEDULE, blockWhileUnscheduled = block) } } diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt index 888eab1453..9e7b1e14dc 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt @@ -129,21 +129,18 @@ internal class SyncOrchestratorImpl @Inject constructor( override fun execute(command: ScheduleCommand): Job = when (command) { is ScheduleCommand.EverythingCommand -> executeSchedulingAction( action = command.action, - blockWhileUnscheduled = command.blockWhileUnscheduled, unschedule = ::cancelBackgroundWork, reschedule = { scheduleBackgroundWork(withDelay = command.withDelay) }, ) is ScheduleCommand.EventsCommand -> executeSchedulingAction( action = command.action, - blockWhileUnscheduled = command.blockWhileUnscheduled, unschedule = ::cancelEventSync, reschedule = { rescheduleEventSync(withDelay = command.withDelay) }, ) is ScheduleCommand.ImagesCommand -> executeSchedulingAction( action = command.action, - blockWhileUnscheduled = command.blockWhileUnscheduled, unschedule = ::stopImageSync, reschedule = { rescheduleImageUpSync() }, ) @@ -214,7 +211,6 @@ internal class SyncOrchestratorImpl @Inject constructor( private fun executeSchedulingAction( action: ScheduleCommand.Action, - blockWhileUnscheduled: (suspend () -> Unit)?, unschedule: () -> Unit, reschedule: suspend () -> Unit, ): Job = when (action) { @@ -224,7 +220,6 @@ internal class SyncOrchestratorImpl @Inject constructor( } ScheduleCommand.Action.RESCHEDULE -> { appScope.launch(ioDispatcher) { - blockWhileUnscheduled?.invoke() reschedule() } } diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/ScheduleSyncCommandTest.kt b/infra/sync/src/test/java/com/simprints/infra/sync/ScheduleSyncCommandTest.kt index 533d314b25..6c32ef6e31 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/ScheduleSyncCommandTest.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/ScheduleSyncCommandTest.kt @@ -14,12 +14,6 @@ class ScheduleSyncCommandTest { assertThat(ScheduleCommand.Everything.unschedule()) .isEqualTo(ScheduleCommand.EverythingCommand(action = ScheduleCommand.Action.UNSCHEDULE)) - - val block: suspend () -> Unit = { } - val command = ScheduleCommand.Everything.rescheduleAfter(withDelay = true, block = block) as ScheduleCommand.EverythingCommand - assertThat(command.action).isEqualTo(ScheduleCommand.Action.RESCHEDULE) - assertThat(command.withDelay).isTrue() - assertThat(command.blockWhileUnscheduled).isSameInstanceAs(block) } @Test @@ -32,12 +26,6 @@ class ScheduleSyncCommandTest { assertThat(ScheduleCommand.Events.unschedule()) .isEqualTo(ScheduleCommand.EventsCommand(action = ScheduleCommand.Action.UNSCHEDULE)) - - val block: suspend () -> Unit = { } - val command = ScheduleCommand.Events.rescheduleAfter(withDelay = false, block = block) as ScheduleCommand.EventsCommand - assertThat(command.action).isEqualTo(ScheduleCommand.Action.RESCHEDULE) - assertThat(command.withDelay).isFalse() - assertThat(command.blockWhileUnscheduled).isSameInstanceAs(block) } @Test @@ -47,10 +35,5 @@ class ScheduleSyncCommandTest { assertThat(ScheduleCommand.Images.unschedule()) .isEqualTo(ScheduleCommand.ImagesCommand(action = ScheduleCommand.Action.UNSCHEDULE)) - - val block: suspend () -> Unit = { } - val command = ScheduleCommand.Images.rescheduleAfter(block) as ScheduleCommand.ImagesCommand - assertThat(command.action).isEqualTo(ScheduleCommand.Action.RESCHEDULE) - assertThat(command.blockWhileUnscheduled).isSameInstanceAs(block) } } diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/SyncOrchestratorCommandExecutionTest.kt b/infra/sync/src/test/java/com/simprints/infra/sync/SyncOrchestratorCommandExecutionTest.kt index e472838ca9..f641d82be7 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/SyncOrchestratorCommandExecutionTest.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/SyncOrchestratorCommandExecutionTest.kt @@ -28,7 +28,6 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.verify import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -204,31 +203,6 @@ class SyncOrchestratorCommandExecutionTest { } } - @Test - fun `rescheduleAfter for schedule events routes to unschedule and reschedule with delay`() = runTest { - every { eventSyncWorkerTagRepository.getAllWorkerTag() } returns "syncWorkers" - every { eventSyncWorkerTagRepository.getPeriodicWorkTags() } returns listOf("tag1", "tag2") - - orchestrator - .execute( - ScheduleCommand.Events.rescheduleAfter(withDelay = true) { }, - ).join() - - verify { - workManager.cancelUniqueWork(EVENT_SYNC_WORK_NAME) - workManager.cancelUniqueWork(EVENT_SYNC_WORK_NAME_ONE_TIME) - workManager.cancelAllWorkByTag("syncWorkers") - workManager.enqueueUniquePeriodicWork( - EVENT_SYNC_WORK_NAME, - any(), - match { - it.workSpec.initialDelay > 0 && - it.tags.containsAll(setOf("tag1", "tag2")) - }, - ) - } - } - @Test fun `unschedule events cancels correct workers`() = runTest { every { eventSyncWorkerTagRepository.getAllWorkerTag() } returns "syncWorkers" @@ -349,41 +323,6 @@ class SyncOrchestratorCommandExecutionTest { verify { workManager.cancelUniqueWork(FILE_UP_SYNC_WORK_NAME) } } - @Test - fun `rescheduleAfter runs block before rescheduling images`() = runTest { - val blockStarted = Channel(Channel.UNLIMITED) - val unblock = Channel(Channel.UNLIMITED) - val block: suspend () -> Unit = { - blockStarted.trySend(Unit) - unblock.receive() - } - - val job = orchestrator.execute(ScheduleCommand.Images.rescheduleAfter(block)) - - verify { workManager.cancelUniqueWork(FILE_UP_SYNC_WORK_NAME) } - - blockStarted.receive() - - verify(exactly = 0) { - workManager.enqueueUniquePeriodicWork( - FILE_UP_SYNC_WORK_NAME, - ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, - any(), - ) - } - - unblock.trySend(Unit) - job.join() - - verify { - workManager.enqueueUniquePeriodicWork( - FILE_UP_SYNC_WORK_NAME, - ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, - any(), - ) - } - } - @Test fun `reschedules image worker when event sync starts`() = runTest { val eventStartFlow = MutableSharedFlow>()