Updated on 2026-08-14
This commit is contained in:
parent
44fc5b9610
commit
fc7b758aff
7 changed files with 220 additions and 11 deletions
|
|
@ -10,15 +10,15 @@ class PeriodicTask<T>(
|
|||
private val task: suspend () -> Result<T>,
|
||||
private val onSuccess: (T) -> Unit,
|
||||
private val onError: (Throwable) -> Unit,
|
||||
private val isDelayFirst: Boolean = false,
|
||||
private val initialDelay: Long = 0L,
|
||||
) {
|
||||
|
||||
private var isActive: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
suspend fun runTaskWithDelay() {
|
||||
isActive.set(true)
|
||||
if (isDelayFirst) {
|
||||
delay(delay)
|
||||
if (initialDelay > 0L) {
|
||||
delay(initialDelay)
|
||||
}
|
||||
while (isActive.get()) {
|
||||
task.invoke()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
package com.tangem.utils.coroutines
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PeriodicTaskTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN initialDelay 0 and delay 1000 WHEN runTaskWithDelay THEN task is invoked immediately`() = runTest {
|
||||
val callCount = AtomicInteger(0)
|
||||
val onSuccess = mockk<(Int) -> Unit>(relaxed = true)
|
||||
val onError = mockk<(Throwable) -> Unit>(relaxed = true)
|
||||
val periodicTask = PeriodicTask(
|
||||
delay = PERIOD,
|
||||
task = { callCount.incrementAndGet(); Result.success(VALUE) },
|
||||
onSuccess = onSuccess,
|
||||
onError = onError,
|
||||
initialDelay = 0L,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
runCurrent()
|
||||
|
||||
assertThat(callCount.get()).isEqualTo(1)
|
||||
periodicTask.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN initialDelay 1000 WHEN runTaskWithDelay THEN task is not invoked before initialDelay elapses`() =
|
||||
runTest {
|
||||
val callCount = AtomicInteger(0)
|
||||
val periodicTask = PeriodicTask(
|
||||
delay = PERIOD,
|
||||
task = { callCount.incrementAndGet(); Result.success(VALUE) },
|
||||
onSuccess = mockk(relaxed = true),
|
||||
onError = mockk(relaxed = true),
|
||||
initialDelay = INITIAL_DELAY,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
advanceTimeBy(INITIAL_DELAY - 1)
|
||||
|
||||
assertThat(callCount.get()).isEqualTo(0)
|
||||
periodicTask.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN initialDelay 1000 WHEN runTaskWithDelay THEN task is invoked once initialDelay elapses`() = runTest {
|
||||
val callCount = AtomicInteger(0)
|
||||
val periodicTask = PeriodicTask(
|
||||
delay = PERIOD,
|
||||
task = { callCount.incrementAndGet(); Result.success(VALUE) },
|
||||
onSuccess = mockk(relaxed = true),
|
||||
onError = mockk(relaxed = true),
|
||||
initialDelay = INITIAL_DELAY,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
advanceTimeBy(INITIAL_DELAY)
|
||||
runCurrent()
|
||||
|
||||
assertThat(callCount.get()).isEqualTo(1)
|
||||
periodicTask.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN periodic task WHEN runTaskWithDelay THEN task is invoked repeatedly every delay ms`() = runTest {
|
||||
val callCount = AtomicInteger(0)
|
||||
val periodicTask = PeriodicTask(
|
||||
delay = PERIOD,
|
||||
task = { callCount.incrementAndGet(); Result.success(VALUE) },
|
||||
onSuccess = mockk(relaxed = true),
|
||||
onError = mockk(relaxed = true),
|
||||
initialDelay = 0L,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
runCurrent()
|
||||
advanceTimeBy(PERIOD * 3)
|
||||
runCurrent()
|
||||
|
||||
assertThat(callCount.get()).isEqualTo(4)
|
||||
periodicTask.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN successful task WHEN runTaskWithDelay THEN onSuccess is called with the result value`() = runTest {
|
||||
val received = AtomicInteger(-1)
|
||||
val errors = AtomicInteger(0)
|
||||
val periodicTask = PeriodicTask(
|
||||
delay = PERIOD,
|
||||
task = { Result.success(VALUE) },
|
||||
onSuccess = { received.set(it) },
|
||||
onError = { errors.incrementAndGet() },
|
||||
initialDelay = 0L,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
runCurrent()
|
||||
|
||||
assertThat(received.get()).isEqualTo(VALUE)
|
||||
assertThat(errors.get()).isEqualTo(0)
|
||||
periodicTask.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN failing task WHEN runTaskWithDelay THEN onError is called with the thrown exception`() = runTest {
|
||||
val boom = IllegalStateException("boom")
|
||||
val captured = arrayOfNulls<Throwable>(1)
|
||||
val successes = AtomicInteger(0)
|
||||
val periodicTask = PeriodicTask<Int>(
|
||||
delay = PERIOD,
|
||||
task = { Result.failure(boom) },
|
||||
onSuccess = { successes.incrementAndGet() },
|
||||
onError = { captured[0] = it },
|
||||
initialDelay = 0L,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
runCurrent()
|
||||
|
||||
assertThat(captured[0]).isSameInstanceAs(boom)
|
||||
assertThat(successes.get()).isEqualTo(0)
|
||||
periodicTask.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN task running WHEN cancel THEN no further invocations happen`() = runTest {
|
||||
val callCount = AtomicInteger(0)
|
||||
val periodicTask = PeriodicTask(
|
||||
delay = PERIOD,
|
||||
task = { callCount.incrementAndGet(); Result.success(VALUE) },
|
||||
onSuccess = mockk(relaxed = true),
|
||||
onError = mockk(relaxed = true),
|
||||
initialDelay = 0L,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
runCurrent()
|
||||
assertThat(callCount.get()).isEqualTo(1)
|
||||
|
||||
periodicTask.cancel()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(callCount.get()).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN initialDelay 1000 and cancel before it elapses WHEN runTaskWithDelay THEN task is never invoked`() =
|
||||
runTest {
|
||||
val callCount = AtomicInteger(0)
|
||||
val periodicTask = PeriodicTask(
|
||||
delay = PERIOD,
|
||||
task = { callCount.incrementAndGet(); Result.success(VALUE) },
|
||||
onSuccess = mockk(relaxed = true),
|
||||
onError = mockk(relaxed = true),
|
||||
initialDelay = INITIAL_DELAY,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
advanceTimeBy(INITIAL_DELAY / 2)
|
||||
periodicTask.cancel()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(callCount.get()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN task cancelled during invocation WHEN runTaskWithDelay THEN onSuccess is not called for the pending result`() =
|
||||
runTest {
|
||||
val onSuccess = mockk<(Int) -> Unit>(relaxed = true)
|
||||
val periodicTask = PeriodicTask(
|
||||
delay = PERIOD,
|
||||
// Simulates a slow task that completes after the scheduler was cancelled.
|
||||
task = {
|
||||
delay(PERIOD)
|
||||
Result.success(VALUE)
|
||||
},
|
||||
onSuccess = onSuccess,
|
||||
onError = mockk(relaxed = true),
|
||||
initialDelay = 0L,
|
||||
)
|
||||
|
||||
launch { periodicTask.runTaskWithDelay() }
|
||||
runCurrent()
|
||||
periodicTask.cancel()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) { onSuccess.invoke(any()) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PERIOD = 10_000L
|
||||
const val INITIAL_DELAY = 1_000L
|
||||
const val VALUE = 42
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +120,6 @@ internal class OnrampSuccessComponentModel @Inject constructor(
|
|||
expressTxStatusTaskScheduler.scheduleTask(
|
||||
modelScope,
|
||||
PeriodicTask(
|
||||
isDelayFirst = false,
|
||||
delay = EXPRESS_STATUS_UPDATE_DELAY,
|
||||
task = {
|
||||
runSuspendCatching {
|
||||
|
|
|
|||
|
|
@ -151,10 +151,14 @@ internal class SwapAmountModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onStart() {
|
||||
val isDelayFirst = params !is SwapAmountComponentParams.AmountBlockParams
|
||||
val initialDelay = if (params is SwapAmountComponentParams.AmountBlockParams) {
|
||||
BLOCK_INITIAL_QUOTE_DELAY
|
||||
} else {
|
||||
QUOTES_UPDATE_DELAY
|
||||
}
|
||||
quoteTaskScheduler.scheduleTask(
|
||||
scope = modelScope,
|
||||
task = loadQuotesTask(isDelayFirst = isDelayFirst),
|
||||
task = loadQuotesTask(initialDelay = initialDelay),
|
||||
)
|
||||
subscribeOnAutoupdateEnabling()
|
||||
}
|
||||
|
|
@ -850,10 +854,10 @@ internal class SwapAmountModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun loadQuotesTask(isDelayFirst: Boolean = true): PeriodicTask<Unit> {
|
||||
private fun loadQuotesTask(initialDelay: Long = QUOTES_UPDATE_DELAY): PeriodicTask<Unit> {
|
||||
return PeriodicTask(
|
||||
delay = QUOTES_UPDATE_DELAY,
|
||||
isDelayFirst = isDelayFirst,
|
||||
initialDelay = initialDelay,
|
||||
task = {
|
||||
runCatching { loadQuotes(isSilentReload = true) }
|
||||
},
|
||||
|
|
@ -933,5 +937,6 @@ internal class SwapAmountModel @Inject constructor(
|
|||
private companion object {
|
||||
const val DEBOUNCE_AMOUNT_DELAY = 500L
|
||||
const val QUOTES_UPDATE_DELAY = 10000L
|
||||
const val BLOCK_INITIAL_QUOTE_DELAY = 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -195,7 +195,6 @@ internal class ExpressTransactionsModel @Inject constructor(
|
|||
expressTxStatusTaskScheduler.scheduleTask(
|
||||
scope = modelScope,
|
||||
task = PeriodicTask(
|
||||
isDelayFirst = false,
|
||||
delay = EXPRESS_STATUS_UPDATE_DELAY,
|
||||
task = {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -390,7 +390,6 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
expressTxStatusTaskScheduler.scheduleTask(
|
||||
scope = modelScope,
|
||||
task = PeriodicTask(
|
||||
isDelayFirst = false,
|
||||
delay = EXPRESS_STATUS_UPDATE_DELAY,
|
||||
task = {
|
||||
runSuspendCatching {
|
||||
|
|
|
|||
|
|
@ -398,7 +398,6 @@ internal class WalletModel @Inject constructor(
|
|||
expressTxStatusTaskScheduler.scheduleTask(
|
||||
modelScope,
|
||||
PeriodicTask(
|
||||
isDelayFirst = false,
|
||||
delay = EXPRESS_STATUS_UPDATE_DELAY,
|
||||
task = {
|
||||
runCatching {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue