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
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.
· 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.
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
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
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.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:
- If the error is network related,
onNetworkException()method instead of handling network related exception itself.
- 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
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:
- maximal attempts count (
- minimal retry interval in milliseconds (
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 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
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
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.
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.