Retrying failed network requests automatically on Android
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:
- If the error is network related,
RetryableExecutor
callsonNetworkException()
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 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:
- maximal attempts count (
maxAttempts
parameter). - 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.