Retrying failed network requests automatically on Android

Image on dev.to

Handling network errors is one of the problems that developers face while creating their apps. In my previous article I discussed handling “No internet connection” errors. Today I am going to review retrying other failed network requests automatically.

For most common network request errors, such as having code of a series of 500 HTTP error codes (server errors), it makes sense to apply a retry policy that implies several trials.

This article will have five parts.

Part 1 — Review of theoretical aspects of retry policy.

Part 2 — Present the base RetryableExecutor class, which encapsulates retry logic.

Part 3 — Present an example of RetryableExecutor’s concrete subclass.

Part 4— Discussion of RetryDelayProvider, which is used by RetryableExecutor to calculate the next retry interval.

Part 5— Discussion of ViewModel example, which uses RetryableExecutor.

The complete example code “Retrying failed network requests automatically on Android” can be found in my AndroidBasicLib in sample.screens.retry subpackage. This library is uploaded to Maven Central, and can be referenced as a remote dependency in an app’s Gradle build script.

1. Retry policy

This topic is well studied in computer science, for example in this article. Below is a short summary.

Avoid anti-patterns:

· Never implement an endless retry mechanism. It would likely prevent the resource or service recovering from overload situations.

· Never perform an immediate retry more than once.

· Avoid using a regular retry interval, especially when you have a large number of retry attempts.

The approach which takes into account all antipatterns listed above, is to have a finite number of attempts with irregular retry intervals. The recommended way to choose a retry interval, is an exponential back-off strategy. This strategy implies that the retry interval grows exponentially, which gives an accessed service an opportunity to recover from overload situations.

Another recommendation is to use retry interval randomization to prevent multiple instances of the client sending subsequent retry attempts at the same time. For example, one client may retry the operation after 2, 5 and 10 seconds, and so on, while another client would do it after 3, 7 and 12 seconds.

2. Base RetryableExecutor class

RetryableExecutor abstract class encapsulates retry logic.

RetryableExecutor takes CoroutineDispatcher and RetryDelayProvider instances as constructor parameters. The former parameter is used as a dispatcher for retrying a request, for example Dispatchers.IO dispatcher. The latter parameter of RetryDelayProvider type, should calculate retry interval based on the attempt index. RetryDelayProvider which implements exponential back-off strategy will be described below. At this point, it is important to know that RetryDelayProvider‘s next() method returns the next retry interval in milliseconds or null if retry attempts are exhausted.

RetryableExecutor provides an ability to start and cancel execution via public execute() and cancel() methods. The most interesting method implemented in this class is a retry() method. Initially, it gets the next retry interval from retryPeriodProvider, then it checks if retry attempts are not exhausted and if executor is not cancelled. If both conditions are met, the function starts a new coroutine on a given dispatcher and waits calculated delayMs milliseconds. Subsequently, if a coroutine is still active (not cancelled) startExecution() method is called with an increase by 1 number of attempts.

3. Example of RetryableExecutor derived class

At this point we will create a RetryableExecutor derived class, which executes a concrete network request.

Let’s discuss a listener, which is one of the constructor parameters of SampleRetryableExecutor. SampleRetryableExecutor.Listener interface extends RetryableExecutor.Listener interface, which contains only onNetworkException() method declaration. The main part of SampleRetryableExecutor class is a startExecution() method implementation. It tries to execute a network operation in a worker thread. If execution is successful, the listener is notified via onSuccess() method call. If an error occurs during network request execution, there are two possible cases:

  1. If the error is network related, RetryableExecutor calls onNetworkException() method instead of handling network related exception itself.
  2. If the error is not network related, retry() method implemented in superclass is called.

Occasionally there are errors which should not lead to retrying requests. In order to handle errors of this sort, new methods can be added to SampleRetryableExecutor.Listener interface.

4. RetryDelayProvider and its subclass ExponentialRetryDelayProvider

Let’s look at abstract class RetryDelayProvider and its concrete subclass, which implements exponential back-off strategy. Let’s take a look under the hood.

RetryDelayProvider’s single public next() method checks if it is the first attempt and returns zero in this case. This means that the first retry would happen immediately. If retry attempts are exhausted, null is returned, otherwise abstract calculate() method is called.

In its turn, ExponentialRetryDelayProvider calculates the next retry interval using exponential back-off strategy in its overridden calculate() method. The formula in this method is simple — the result value is some random number between the adjacent powers of 2.

It is necessary to specify the following parameters while creating ExponentialRetryDelayProvider class instance:

  1. maximal attempts count (maxAttempts parameter).
  2. minimal retry interval in milliseconds (minimalDelayMs parameter).

You can tune these parameters to the needs of your app. Currently I set 10 attempts for the former parameter and 500 milliseconds (half of a second) for the latter.

5. Using RetryableExecutor in ViewModel

In order to retry failed network requests, I am going to modify MyScreenModel class created in the previous article.

latestRetryableExecutor member has SynchronizedValue<SampleRetryableExecutor> type. SynchronizedValue encapsulates read/write lock in order to synchronize multithreaded data accesses and is available in my AndroidBasicLib as well.

Most of the changes compared with the variant described in the previous article, are in startRequest() method. Instead of performing a network request directly, it is done via created SampleRetryableExecutor instance. In a listener, provided as a SampleRetryableExecutor constructor parameter, two cases are handled: successful operation finish and network related exception. Please note, both listener methods are called in a worker thread, which is why it is necessary to synchronize latestRetryableExecutor access.

You can notice that MyScreenModelV2 is quite concise and its code is not polluted with request retrying logic.

Generally, it is a poor practice to hardcode concrete numbers, which are passed as maxAttempts and minimalDelayMs parameters to SampleRetryableExecutor’s constructor. In MyScreenModelV2 I do so for the sake of brevity, but in the mentioned library these constants are encapsulated in RetryPolicy class.

Conclusion

Now you have both theoretical knowledge and practical instruments to perform network requests reliably in your Android app.

Please tell me about the retry policy you use in your application in the comments section.

Senior Android Developer at AltoPass