Search code examples
androidkotlinasync-awaitretrofit2kotlin-coroutines

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


Solution

  • 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.