Photo by Dewang Gupta on Unsplash

Rxify: Retry with Exponential Backoff in RxJava

Garima Jain
Mar 6, 2019 · 3 min read

At Over, we faced an issue wherein the app was running OutOfMemory when trying to export a project with high-resolution images. This made us think of various approaches for retrying our export mechanism.

Whenever we think of retrying, there are quite a few options available to us. Create a custom solution with recursion perhaps or maybe an iterative approach. One could also come up with a solution with co-routines. It all depends upon the implementation of the crash-site. In our case, exportAt(scale) is the crash-site.

Fortunately (or unfortunately — whichever way you look at it 😜) our exportAt(scale) function is reactive. Even with Rx, for retrying we have multiple options to choose from depending upon the problem statement. Let us have a look at the problem statement then.

Problem:

What are our options here? With Rx we have an operator called retry() which has a few overloaded options. Our problem demands that we change our original Single exportAt(scale) each time we want to retry() to take a different scale. None of the overloaded variations of the retry() operator would let us change our scale as a function of the number of times we are retrying (At least from what I could make out from the documentation 🤔). So, we need to think of another way.

Solution:

Range : Observable.range(0, 5)

ConcatMap :

TakeUntil : (`Take`um `Until`lum ;)

Here’s the full code-snippet with exponential backoff :

  1. Observable.range emits values 0 to 5, where 5 is the max. number of times we wish to retry.
  2. map() converts our input values into scale = 1/2^(input) i.e.1.0, 0.5, 0.25 …. (Reducing exponentially). You can provide it any function, depending upon the retry mechanism you choose. Linear for example could look like : scale = 1.0f — 0.1 * input producing values 1.0 , 0.9, 0.8, 0.7 and so on.
  3. concatMap() subscribes to the next scale value only when one value completes. So, we will not subscribe to exportAt(0.9) until we are done processing exportAt(1.0).
  4. Then, map() and onErrorReturn() wrap our exportAt(scale) output inside a Result wrapper which is a sealed class as follows :
sealed class Result {
data class Success(val double: Double): Result()
data class Failed(val error: Throwable): Result()
}

5. With takeUntil(Result.Success): We keep on retrying till our exportAt(scale) successfully exports the Project and thus emits a Result.Success.

6. Finally, with lastOrError() we take the last value which will be Result.Success in case we were successful in 5 attempts. Or we get our exportAt(scale) exception wrapped inside Result.Error in case we couldn’t succeed even in 5 attempts.

7. We can then show User appropriate feedback depending upon whether we were successful in exporting the project or not.

And with this, we are done!

Source

This wraps up our approach to implementing exponential backoff with Rx. I am sure that there are alternative solutions to this problem, let me know if you find a different or better way in the comments below.

Hope it helps. Find me on twitter @ragdroid.

Over Engineering

Stories from the engineers @ Over

Over Engineering

Create professional ads, branded content, and stunning stories in minutes.

Garima Jain

Written by

Android @GoDaddy Studio

Over Engineering

Create professional ads, branded content, and stunning stories in minutes.