How to Exponential Backoff retry on kotlin coroutines How to Exponential Backoff retry on kotlin coroutines android android

How to Exponential Backoff retry on kotlin coroutines


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.


You can try this simple but very agile approach with simple usage:

EDIT: added a more sophisticated solution in a separate answer.

class Completion(private val retry: (Completion) -> Unit) {    fun operationFailed() {        retry.invoke(this)    }}fun retryOperation(retries: Int,                    dispatcher: CoroutineDispatcher = Dispatchers.Default,                    operation: Completion.() -> Unit) {    var tryNumber = 0    val completion = Completion {        tryNumber++        if (tryNumber < retries) {            GlobalScope.launch(dispatcher) {                delay(TimeUnit.SECONDS.toMillis(tryNumber.toLong()))                operation.invoke(it)            }        }    }    operation.invoke(completion)}

The use it like this:

retryOperation(3) {    if (!tryStuff()) {        // this will trigger a retry after tryNumber seconds        operationFailed()    }}

You can obviously build more on top of it.


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()        }    }}