59

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 ??

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
shakil.k
  • 1,623
  • 5
  • 17
  • 27

7 Answers7

211

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.

Roman Elizarov
  • 27,053
  • 12
  • 64
  • 60
  • 1
    This was something that was at the back of my head for some days now. Nice to see the solution is not more complex than what I was imagining. I was also asking myself whether it would make sense to define this helper function as an inline function. And last but not least: how would the a be modified, if you want to execute the retry only after asking the user to do so (eg. in a dialogue)? – Fatih Coşkun Oct 26 '17 at 21:51
  • 1
    Also so much cleaner than the Rx solution :-O – kenyee Nov 07 '17 at 17:55
  • 2
    if https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/retry.html would also emit the current amount of retries it could be used to make a clean (exponential) back-off with it – ligi Nov 03 '19 at 02:49
4

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)
    ...
}
diAz
  • 478
  • 4
  • 16
2

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()
        }
    }
}
Sir Codesalot
  • 7,045
  • 2
  • 50
  • 56
1

Flow Version https://github.com/hoc081098/FlowExt

package com.hoc081098.flowext

import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.retryWhen

@ExperimentalTime
public fun <T> Flow<T>.retryWithExponentialBackoff(
  initialDelay: Duration,
  factor: Double,
  maxAttempt: Long = Long.MAX_VALUE,
  maxDelay: Duration = Duration.INFINITE,
  predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T> {
  require(maxAttempt > 0) { "Expected positive amount of maxAttempt, but had $maxAttempt" }
  return retryWhenWithExponentialBackoff(
    initialDelay = initialDelay,
    factor = factor,
    maxDelay = maxDelay
  ) { cause, attempt -> attempt < maxAttempt && predicate(cause) }
}

@ExperimentalTime
public fun <T> Flow<T>.retryWhenWithExponentialBackoff(
  initialDelay: Duration,
  factor: Double,
  maxDelay: Duration = Duration.INFINITE,
  predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean
): Flow<T> = flow {
  var currentDelay = initialDelay

  retryWhen { cause, attempt ->
    predicate(cause, attempt).also {
      if (it) {
        delay(currentDelay)
        currentDelay = (currentDelay * factor).coerceAtMost(maxDelay)
      }
    }
  }.let { emitAll(it) }
}

1

Improved version of @Roman Elizarov's answer using an Iterator/Sequence for the backoff strategy:

private suspend fun <T> retry(
    times: Int = 3,              // retry three times
    backoffStrategy: Iterator<Long>,
    predicate: (R) -> Boolean = { false },
    block: suspend (attempt: Int) -> T): T
{
    repeat(times - 1) { attempt ->
        val result = block(attempt + 1)
        if (predicate(result)) {
            delay(backoffStrategy.next())
        } else {
            return result
        }
    }
    return block(times) // last attempt
}

Using the Iterator separates the retry logic from the backoff strategy which can be as simple as:

// generates 1000, 1000, 1000 etc.
val linearBackoff = generateSequence(1000) { it }.iterator()

or more sophisticated:

val exponentialBackoff = backoffStrategy()
val constantBackoff = backoffStrategy(factor = 1.0)

fun backoffStrategy(
    initialDelay: Long = 1000,   // 1 second
    maxDelay: Long = 20000,      // 10 second
    factor: Double = 2.0,        // exponential backoff base 2
) = generateSequence(initialDelay) { previous ->
    previous.times(factor).toLong().coerceAtMost(maxDelay)
}.iterator()

Note: the code to be executed (block) is responsible for handling exceptions. I typically do railway oriented programming so T is something like Either<Error, T> or Result<T>.

Emanuel Moecklin
  • 28,488
  • 11
  • 69
  • 85
  • `runCatching` is maybe a bit dangerous. Will it swallow CancellationExceptions? – Tenfour04 Mar 30 '23 at 13:16
  • @Tenfour04 thanks for pointing this out. In my "real" code I use another parameter `predicate: (R) -> Boolean = { false },` that decides whether the result is Ok or NOK -> `block` is responsible for handling exceptions. I updated my answer accordingly. – Emanuel Moecklin Mar 30 '23 at 14:29
0

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.

Sir Codesalot
  • 7,045
  • 2
  • 50
  • 56
0

Here’s a Kotlin algorithm that uses retryWhen to retry network requests with increasing delays between attempts:

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.retryWhen
import java.net.SocketTimeoutException

suspend fun <T> Flow<T>.retryWithBackoff(
    maxAttempts: Int,
    initialDelayMillis: Long,
    maxDelayMillis: Long,
    factor: Double,
    predicate: (Throwable) -> Boolean = { it is SocketTimeoutException }
): Flow<T> = retryWhen { cause, attempt ->
    if (attempt >= maxAttempts || !predicate(cause)) {
        throw cause
    }
    delay(initialDelayMillis * factor.pow(attempt.toDouble()).toLong().coerceAtMost(maxDelayMillis))
}

This function extends the Flow class with a retryWithBackoff function that takes in an integer maxAttempts that specifies the maximum number of times the function should attempt to connect to the server, a initialDelayMillis long that specifies the initial delay before the first attempt, a maxDelayMillis long that specifies the maximum delay between attempts, a factor double that specifies the factor by which the delay should increase between attempts, and an optional predicate function that determines whether to retry on a given exception.

The function retries network requests using retryWhen, which resubscribes to the flow when an exception occurs and the predicate returns true. The delay between retries increases exponentially with each attempt, up to a maximum delay specified by maxDelayMillis.

Here’s an example usage of this function:

val flow = // your flow here

flow.retryWithBackoff(maxAttempts = 5, initialDelayMillis = 1000L, maxDelayMillis = 10000L, factor = 2.0)
    .catch { e -> println("Failed after 5 attempts: ${e.message}") }
    .collect { value -> println("Value: $value") }

This code will retry the network request up to 5 times with an initial delay of 1000 milliseconds and a maximum delay of 10000 milliseconds between attempts. The delay between attempts will increase exponentially with a factor of 2.0.

Generated by Bing Ai