commonMain.net.folivo.trixnity.client.utils.retryLoopFlow.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of trixnity-client-jvm Show documentation
Show all versions of trixnity-client-jvm Show documentation
Multiplatform Kotlin SDK for matrix-protocol
package net.folivo.trixnity.client.utils
import arrow.resilience.Schedule
import arrow.resilience.retry
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import net.folivo.trixnity.client.utils.RetryLoopFlowState.*
import net.folivo.trixnity.clientserverapi.client.SyncState
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.ZERO
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
private val log = KotlinLogging.logger { }
enum class RetryLoopFlowState {
RUN, PAUSE, STOP,
}
private interface RetryLoopFlowResult {
object Suspend : RetryLoopFlowResult
class Emit(val value: T) : RetryLoopFlowResult
}
suspend fun retryLoopFlow(
requestedState: Flow,
scheduleBase: Duration = 100.milliseconds,
scheduleFactor: Double = 2.0,
scheduleLimit: Duration = 5.minutes,
onError: suspend (error: Throwable) -> Unit = {},
onCancel: suspend () -> Unit = {},
block: suspend () -> T
): Flow = flow {
coroutineScope {
val state = MutableSharedFlow(1)
val stateJob = launch { requestedState.collectLatest { state.emit(it) } }
val schedule = Schedule.exponential(scheduleBase, scheduleFactor)
// .or(Schedule.spaced(scheduleLimit)) // works again in a future version of arrow
.or(Schedule.spaced(scheduleLimit), transform = ::Pair) { a, b -> minOf(a ?: ZERO, b ?: ZERO) }
.and(Schedule.doWhile { _, _ -> state.first() == RUN })
.log { input, _ ->
if (input !is CancellationException) onError(input)
}
while (currentCoroutineContext().isActive) {
try {
val shouldStop = state.transform {
when (it) {
RUN -> emit(false)
PAUSE -> {} // don't emit and therefore wait for next state
STOP -> emit(true)
}
}.first()
if (shouldStop) break
emit(
RetryLoopFlowResult.Emit(
schedule.retry {
block()
}
)
)
yield()
emit(RetryLoopFlowResult.Suspend) // if we don't do that, block may be called even if not needed
} catch (error: Exception) {
if (error is CancellationException) {
onCancel()
throw error
}
log.debug { "retry loop operation after exception" }
}
}
stateJob.cancel()
}
}.buffer(0)
.transform {
if (it is RetryLoopFlowResult.Emit) emit(it.value)
}
suspend fun retryLoop(
requestedState: Flow,
scheduleBase: Duration = 100.milliseconds,
scheduleFactor: Double = 2.0,
scheduleLimit: Duration = 5.minutes,
onError: suspend (error: Throwable) -> Unit = {},
onCancel: suspend () -> Unit = {},
block: suspend () -> Unit
): Unit = retryLoopFlow(
requestedState = requestedState,
scheduleBase = scheduleBase,
scheduleFactor = scheduleFactor,
scheduleLimit = scheduleLimit,
onError = onError,
onCancel = onCancel,
block = block
).collect()
suspend fun retryWhen(
requestedState: Flow,
scheduleBase: Duration = 100.milliseconds,
scheduleFactor: Double = 2.0,
scheduleLimit: Duration = 5.minutes,
onError: suspend (error: Throwable) -> Unit = {},
onCancel: suspend () -> Unit = {},
block: suspend () -> T
): T = retryLoopFlow(
requestedState = requestedState,
scheduleBase = scheduleBase,
scheduleFactor = scheduleFactor,
scheduleLimit = scheduleLimit,
onError = onError,
onCancel = onCancel,
block = block
).first()
suspend fun StateFlow.retryWhenSyncIs(
syncState: SyncState,
vararg moreSyncStates: SyncState,
scheduleBase: Duration = 100.milliseconds,
scheduleFactor: Double = 2.0,
scheduleLimit: Duration = 5.minutes,
onError: suspend (error: Throwable) -> Unit = {},
onCancel: suspend () -> Unit = {},
block: suspend () -> T
): T = coroutineScope {
val syncStates = listOf(syncState) + moreSyncStates
retryWhen(
requestedState = map { if (syncStates.contains(it)) RUN else PAUSE },
scheduleBase = scheduleBase,
scheduleFactor = scheduleFactor,
scheduleLimit = scheduleLimit,
onError = onError,
onCancel = onCancel,
block = block
)
}
suspend fun StateFlow.retryLoopWhenSyncIs(
syncState: SyncState,
vararg moreSyncStates: SyncState,
scheduleBase: Duration = 100.milliseconds,
scheduleFactor: Double = 2.0,
scheduleLimit: Duration = 5.minutes,
onError: suspend (error: Throwable) -> Unit = {},
onCancel: suspend () -> Unit = {},
block: suspend () -> Unit
) {
val syncStates = listOf(syncState) + moreSyncStates
retryLoop(
requestedState = map { if (syncStates.contains(it)) RUN else PAUSE },
scheduleBase = scheduleBase,
scheduleFactor = scheduleFactor,
scheduleLimit = scheduleLimit,
onError = onError,
onCancel = onCancel,
block = block
)
}