All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.net.folivo.trixnity.client.utils.retryLoopFlow.kt Maven / Gradle / Ivy

There is a newer version: 4.7.1
Show newest version
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
    )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy