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