Kevin Marlow

Mobile Engineer

Engineering Manager at Sentio. We turn Android smartphones into work computers.


Better Exponential Backoffs with RxJava and Retrofit on Android

This article assumes that the reader is familiar with RxJava and Retrofit. It also makes use of Observable responses from the generated API interface. If you are unfamiliar with these topics, check of the following sources.

Let's face it, networking on mobile can be difficult at times. It is nearly impossible to guess what new thing may be causing network connectivity issues for your users. Enter the exponential back-off. For those of you who are unfamiliar how exponential back-offs work, let's look at a common scenario with mobile.

  1. Your networking code makes a call to your awesome API.
  2. Uh oh, looks like a timeout happened, better retry.
  3. Oh no, another timeout, should I retry again?

The answer is usually "it depends". For many of our GET requests, and some of our POST requests, retrying makes sense. However, it would be better if we could wait a little longer between each retry. Maybe something like this.

  1. First network call -> timeout -> retry in 5 seconds.
  2. Second network call -> timeout again -> retry in 25 seconds.
  3. Third network call -> timeout yet again -> last retry in ~2 minutes.

Hmm... that looks a lot like the function 5^x where x is the attempt count. How could we write that in Android? Well, if you are using RxJava and Retrofit, you have probably already read this article by Dan Lew.

In it, he shows how zipWith and range can be combine to create a basic implementation of exponential back-offs. But what if I only want to retry on timeouts you say! Well, he also shows how you can use retryWhen and instanceof to allow certain error types to pass through.

But what if I want to do both of those things? And what if I want to pass the error back if all of my retries fail? Ideally, I just want to write the following:

source  
    .retryWhen(exponentialBackoffForExceptions(initialDelay, retryCount, 
        TimeUnit.SECONDS, IOException.class))
    .subscribe(...);



Exponential back-off with error type checks

With a little help from the android.util.Pair class, here is what a combination of the two looks like.

    private static final int UNCHECKED_ERROR_TYPE_CODE = -100;

@SafeVarargs
public static Func1<Observable<? extends Throwable>, Observable<?>>  
    exponentialBackoffForExceptions(final long initialDelay, final int numRetries,
    final TimeUnit unit, final Class<? extends Throwable>... errorTypes) {

    if (initialDelay <= 0) {
        throw new IllegalArgumentException("initialDelay must be greater than 0");
    }

    if (numRetries <= 0) {
        throw new IllegalArgumentException("numRetries must be greater than 0");
    }

    return errors -> errors
            .zipWith(Observable.range(1, numRetries + 1), (error, integer) -> {
                if (integer == numRetries + 1) {
                    return new Pair<>(error, UNCHECKED_ERROR_TYPE_CODE);
                }

                if (errorTypes != null) {
                    for (Class<? extends Throwable> clazz : errorTypes) {
                        if (clazz.isInstance(error)) {
                            // Mark as error type found
                            return new Pair<>(error, integer);
                        }
                    }
                }

                return new Pair<>(error, UNCHECKED_ERROR_TYPE_CODE);
            })
            .flatMap(errorRetryCountTuple -> {

                int retryAttempt = errorRetryCountTuple.second;

                // If not a known error type, pass the error through.
                if (retryAttempt == UNCHECKED_ERROR_TYPE_CODE) {
                    return Observable.error(errorRetryCountTuple.first);
                }

                long delay = (long) Math.pow(initialDelay, retryAttempt);

                // Else, exponential backoff for the passed in error types.
                return Observable.timer(delay, unit);
            });
}

Wow, thats complicated. Okay, let's break it down.



The Breakdown

public static Func1<Observable<? extends Throwable>, Observable<?>>  
    exponentialBackoffForExceptions(final long initialDelay, final int numRetries,
    final TimeUnit unit, final Class<? extends Throwable>... errorTypes)

Our function will take an initialDelay, a numRetries, the TimeUnit for our delays, and a list of Throwable error types to retry on. All of this makes up the function that will get used with retryWhen in your chain.

return errors -> errors  

If you read Dan Lew's article, you know that this is the Func1<? super Observable<? extends Throwable>, ? extends Observable<?> that retryWhen takes in as a parameter. The easy way to think about it is that it is the Observable for the error that would normally be passed into onError.

.zipWith(Observable.range(1, numRetries + 1), (error, integer) -> {
    if (integer == numRetries + 1) {
        return new Pair<>(error, UNCHECKED_ERROR_TYPE_CODE);
    }

    if (errorTypes != null) {
        for (Class<? extends Throwable> clazz : errorTypes) {
            if (clazz.isInstance(error)) {
                // Mark as error type found
                return new Pair<>(error, integer);
            }
        }
    }

    return new Pair<>(error, UNCHECKED_ERROR_TYPE_CODE);
})

Here, we zip the errors passed into retryWhen with a range from 1 to our numRetries plus 1. This extra number allows us to check for and pass the final error through as an error. Note that we need a flag to keep track of that. Here we use UNCHECKED_ERROR_TYPE_CODE.

The first check determines if we have reached the end of our retry count, if so, mark it as such and return.

The second check iterates through our passed-in errorType list to see if the error matches any that we are checking for. If it does, pass the error and the retryCount.

Lastly, if it is not an error type that we are retrying with, pass it through.

.flatMap(errorRetryCountTuple -> {

    int retryAttempt = errorRetryCountTuple.second;

    // If not a known error type, pass the error through.
    if (retryAttempt == UNCHECKED_ERROR_TYPE_CODE) {
        return Observable.error(errorRetryCountTuple.first);
    }

    long delay = (long) Math.pow(initialDelay, retryAttempt);

    // Else, exponential backoff for the passed in error types.
    return Observable.timer(delay, unit);
})

The last part uses flatMap to check each android.util.Pair that is passed from zipWith. We get the retryAttempt and check to see if it's code is the UNCHECKED_ERROR_TYPE_CODE. If it is, return the error to retryWhen, so that it can be passed on to the caller's onError.

If the retryAttempt does not match UNCHECKED_ERROR_TYPE_CODE, calculate the delay and return an Observable.timer to retryWhen, so that it retries after the newest calculated delay.



Conclusion

With the use of our new function, we can now include smart exponential back-offs in any of our network calls. All we have to do is write:

source  
    .retryWhen(exponentialBackoffForExceptions(initialDelay, retryCount, 
        TimeUnit.SECONDS, IOException.class))
    .subscribe(...);



Much credit to Dan Lew for all of his expert insights into RxJava on the Android platform.