Kotlin Flow Retry Operator with Exponential Backoff Delay

Kotlin Flow Retry Operator with Exponential Backoff Delay

In this tutorial, we are going to learn about the Kotlin Flow retry and retryWhen operators and how to retry the task with exponential backoff delay using it.

Before starting, for your information, this blog post is a part of the series that we are writing on Flow APIs in Kotlin Coroutines.

Resources to get started with Kotlin Flow:

Let's get started.

First, let's go through the operators available in Kotlin Flow for retrying the task. Currently, there are two operators present in Kotlin Flow, both can be used interchangeably in most of the cases.

  • retryWhen
  • retry

retryWhen

This is the definition of the retryWhen Flow operator.

fun <T> Flow<T>.retryWhen(predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean): Flow<T>

And, we use like below:

.retryWhen { cause, attempt ->
    
}

Here, we receive the following two parameters:

  • cause: This cause is Throwable which is the base class for all errors and exceptions.
  • attempt: This attempt is the number that represents the current attempt. It starts with zero.

Suppose there is an exception when we started the task, we will receive the cause(exception) and attempt(0).

The retryWhen takes a predicate function to decide whether to retry or not.

If it returns true, then only it will retry else it will not.

For example, we can do like below:

.retryWhen { cause, attempt ->
    if (cause is IOException && attempt < 3) {
        delay(2000)
        return@retryWhen true
    } else {
        return@retryWhen false
    }
}

Here, we are returning true when the cause is IOException, and attempt count is less than 3.

So, it will only retry if the condition is satisfied.

Note: As the predicate function is suspending function, we can call another suspending function from it.

If we notice in the above code, we have called delay(2000), so that it retries only after a delay of 2 seconds.

retry

This is the definition of the retry Flow operator.

fun <T> Flow<T>.retry(
    retries: Long = Long.MAX_VALUE,
    predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T>

The complete block from the source code of Kotlin Flow.

fun <T> Flow<T>.retry(
    retries: Long = Long.MAX_VALUE,
    predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T> {
    require(retries > 0) { "Expected positive amount of retries, but had $retries" }
    return retryWhen { cause, attempt -> attempt < retries && predicate(cause) }
}

If we see the retry function, it actually calls the retryWhen internally.

retry function has default arguments.

  • If we do not pass the retries, it will use the Long.MAX_VALUE.
  • If we do not pass the predicate, it will provide true.

For example, we can do like below:

.retry()

It will keep retrying until the task gets completed successfully.

For example, we can also do like below:

.retry(3)

It will only retry 3 times.

For example, we can also do like below:

.retry(retries = 3) { cause ->
    if (cause is IOException) {
        delay(2000)
        return@retry true
    } else {
        return@retry false
    }
}

Here, it becomes very similar to what we did using the retryWhen above.

Here, we are returning true when the cause is IOException. So, it will only retry when the cause is IOException.

If we notice in the above code, we have called delay(2000), so that it retries only after a delay of 2 seconds.

Now, let's see the code examples.

I will be using this project for the implementation part. You can find the complete code for the implementation mentioned in this blog in the project itself.

This is a function to simulate a long-running task with exceptions.

private fun doLongRunningTask(): Flow<Int> {
    return flow {
        // your code for doing a long running task
        // Added delay, random number, and exception to simulate

        delay(2000)

        val randomNumber = (0..2).random()

        if (randomNumber == 0) {
            throw IOException()
        } else if (randomNumber == 1) {
            throw IndexOutOfBoundsException()
        }

        delay(2000)
        emit(0)
    }
}

Now, when using the retry operator

viewModelScope.launch {
    doLongRunningTask()
        .flowOn(Dispatchers.Default)
        .retry(retries = 3) { cause ->
            if (cause is IOException) {
                delay(2000)
                return@retry true
            } else {
                return@retry false
            }
        }
        .catch {
           // error
        }
        .collect {
            // success
        }
}

Similarly, when using the retryWhen operator

viewModelScope.launch {
    doLongRunningTask()
        .flowOn(Dispatchers.Default)
        .retryWhen { cause, attempt ->
            if (cause is IOException && attempt < 3) {
                delay(2000)
                return@retryWhen true
            } else {
                return@retryWhen false
            }
        }
        .catch {
            // error
        }
        .collect {
            // success
        }
}

If we see, every time we are adding the delay of 2 seconds, but in real use-cases, we add delay with exponential backoff. Do not worry, we will implement that too.

Retry Operator with Exponential Backoff Delay

After adding the code for the delay with exponential backoff

viewModelScope.launch {
    var currentDelay = 1000L
    val delayFactor = 2
    doLongRunningTask()
        .flowOn(Dispatchers.Default)
        .retry(retries = 3) { cause ->
            if (cause is IOException) {
                delay(currentDelay)
                currentDelay = (currentDelay * delayFactor)
                return@retry true
            } else {
                return@retry false
            }
        }
        .catch {
            // error
        }
        .collect {
            // success
        }
}

Here, we have created two variables:

  • currentDelay: This represents the delay to be used in the current retry.
  • delayFactor: We use this delayFactor to multiply it with currentDelay to increase the delay for the next retry.

That's it, we have implemented the retry with exponential backoff delay.

You can build, run, play all the examples in the project provided.

This way we can use retry and retryWhen Operators of Flow to solve the interesting problem in Android App Development. Remember both can be used interchangeably in most cases that we solve in Android App Development.

That's it for now.

Happy Learning :)

Show your love by sharing this blog with your fellow developers.

Also, Let’s become friends on Twitter, Linkedin, Github, Quora, and Facebook.