How to Exponential Backoff retry on kotlin coroutines
Here's a more sophisticated and convenient version of my previous answer, hope it helps someone:
class RetryOperation internal constructor(
private val retries: Int,
private val initialIntervalMilli: Long = 1000,
private val retryStrategy: RetryStrategy = RetryStrategy.LINEAR,
private val retry: suspend RetryOperation.() -> Unit
) {
var tryNumber: Int = 0
internal set
suspend fun operationFailed() {
tryNumber++
if (tryNumber < retries) {
delay(calculateDelay(tryNumber, initialIntervalMilli, retryStrategy))
retry.invoke(this)
}
}
}
enum class RetryStrategy {
CONSTANT, LINEAR, EXPONENTIAL
}
suspend fun retryOperation(
retries: Int = 100,
initialDelay: Long = 0,
initialIntervalMilli: Long = 1000,
retryStrategy: RetryStrategy = RetryStrategy.LINEAR,
operation: suspend RetryOperation.() -> Unit
) {
val retryOperation = RetryOperation(
retries,
initialIntervalMilli,
retryStrategy,
operation,
)
delay(initialDelay)
operation.invoke(retryOperation)
}
internal fun calculateDelay(tryNumber: Int, initialIntervalMilli: Long, retryStrategy: RetryStrategy): Long {
return when (retryStrategy) {
RetryStrategy.CONSTANT -> initialIntervalMilli
RetryStrategy.LINEAR -> initialIntervalMilli * tryNumber
RetryStrategy.EXPONENTIAL -> 2.0.pow(tryNumber).toLong()
}
}
Usage:
coroutineScope.launch {
retryOperation(3) {
if (!tryStuff()) {
Log.d(TAG, "Try number $tryNumber")
operationFailed()
}
}
}
I would suggest to write a helper higher-order function for your retry logic. You can use the following implementation for a start:
suspend fun <T> retryIO(
times: Int = Int.MAX_VALUE,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 1000, // 1 second
factor: Double = 2.0,
block: suspend () -> T): T
{
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: IOException) {
// you can log an error here and/or make a more finer-grained
// analysis of the cause to see if retry is needed
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block() // last attempt
}
Using this function is very strightforward:
val networkResult = retryIO { api.getArticle().await() }
You can change retry parameters on case-by-case basis, for example:
val networkResult = retryIO(times = 3) { api.doSomething().await() }
You can also completely change the implementation of retryIO
to suit the needs of your application. For example, you can hard-code all the retry parameters, get rid of the limit on the number of retries, change defaults, etc.
Here an example with the Flow
and the retryWhen
function
RetryWhen
Extension :
fun <T> Flow<T>.retryWhen(
@FloatRange(from = 0.0) initialDelay: Float = RETRY_INITIAL_DELAY,
@FloatRange(from = 1.0) retryFactor: Float = RETRY_FACTOR_DELAY,
predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long, delay: Long) -> Boolean
): Flow<T> = this.retryWhen { cause, attempt ->
val retryDelay = initialDelay * retryFactor.pow(attempt.toFloat())
predicate(cause, attempt, retryDelay.toLong())
}
Usage :
flow {
...
}.retryWhen { cause, attempt, delay ->
delay(delay)
...
}