Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Exponential Backoff retry on kotlin coroutines

I am using kotlin coroutines for network request using extension method to call class in retrofit like this

public suspend fun <T : Any> Call<T>.await(): T {

  return suspendCancellableCoroutine { continuation -> 

    enqueue(object : Callback<T> {

        override fun onResponse(call: Call<T>?, response: Response<T?>) {
            if (response.isSuccessful) {
                val body = response.body()
                if (body == null) {
                    continuation.resumeWithException(
                            NullPointerException("Response body is null")
                    )
                } else {
                    continuation.resume(body)
                }
            } else {
                continuation.resumeWithException(HttpException(response))
            }
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
            // Don't bother with resuming the continuation if it is already cancelled.
            if (continuation.isCancelled) return
            continuation.resumeWithException(t)
        }
    })

      registerOnCompletion(continuation)
  }
}

then from calling side i am using above method like this

private fun getArticles()  = launch(UI) {

    loading.value = true
    try {
        val networkResult = api.getArticle().await()
        articles.value =  networkResult

    }catch (e: Throwable){
        e.printStackTrace()
        message.value = e.message

    }finally {
        loading.value = false
    }

}

i want to exponential retry this api call in some case i.e (IOException) how can i achieve it ??

like image 414
shakil.k Avatar asked Oct 22 '17 08:10

shakil.k


People also ask

Can coroutines in Kotlin be suspended and resumed mid execution?

Coroutines can suspend themselves, and the dispatcher is responsible for resuming them. To specify where the coroutines should run, Kotlin provides three dispatchers that you can use: Dispatchers. Main - Use this dispatcher to run a coroutine on the main Android thread.

What is runBlocking in coroutines?

Definition of runBlocking() functionRuns a new coroutine and blocks the current thread interruptible until its completion. This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests.

Do Kotlin coroutines run in parallel?

Kotlin's Coroutines enabling you to write parallel code easily, in a sequential way, and without worrying about the contextual overheads you know from using threads in Java. In this article, I want to give an introduction on how to get started quickly, so you can make actual use of it in one of your projects.

How does exponential backoff work?

An exponential backoff algorithm retries requests exponentially, increasing the waiting time between retries up to a maximum backoff time. For example: Make a request to Cloud IoT Core. If the request fails, wait 1 + random_number_milliseconds seconds and retry the request.


3 Answers

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.

like image 152
Roman Elizarov Avatar answered Oct 17 '22 05:10

Roman Elizarov


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()
        }
    }
}
like image 23
Sir Codesalot Avatar answered Oct 17 '22 05:10

Sir Codesalot


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)
    ...
}
like image 4
diAz Avatar answered Oct 17 '22 06:10

diAz