There are several problems with your approach let me try to describe them
Not all operations are idempotent
Based on your shared code I assume your MakeRequestEx
function is generic enough to issue request with GET
or POST
verbs. If you blindly apply retry logic against any kind of request then you might end up with unwanted duplicates or irreversible side-effects.
That's why in order to use retry logic you should meet these criteria group:
- The potentially introduced observable impact is acceptable
- The operation can be redone without any irreversible side effect
- The introduced complexity is negligible compared to the promised reliability
Most of the time the GET
operations are written in idempotent way, but the other (CRU) operations aren't. Please make sure that your downstream system support request de-duplication for all verbs if you want to use your policy + MakeRequestEx
as it is.
Not all errors are transient
Based on your shared code you want to retry if an exception is thrown. The problem with this approach is that not all exceptions represent a transient/temporal issue.
If you re-execute an operation like 10/0
then you will always receive a DivideByZeroException
no matter how many times you want to retry it. The same is true for http request. For example if the url is invalid then you would receive an UrlFormatException
. If you re-do it N times it will be still an UrlFormatException
because it is not a transient failure.
Most of the time it is sufficient to re-do http requests if you receive HttpRequestException
or when the response status code is one of these: 408, 429 or 5XX.
Counting the retries
when we have failed 5 times I know the user doesn't have internet so I want to display a messagebox saying 'App requires internet'
The retry policy's Execute
and ExecuteAsync
methods work in the way that if the operation can't be successfully completed after N retries they throw the last attempt's exception.
So, if you wrap your ExecuteAsync
with a try
-catch
block then you can be sure that all retries has failed when the execution flows into the catch
block.
try
{
await policy.ExecuteAsync(() => MakeRequestEx<T(...));
}
catch
{
MessageBox.Show("Operation failed after 6 attempts");
}
In the message of the MessageBox
I have written 6 attempts since you have configured your policy with 5 retries but there is also the initial (0th) attempt.
If you don't want to use try
-catch
block then you can use the ExecuteAndCapture
/ExecuteAndCaptureAsync
method. These method will not throw an exception if all retry attempts failed rather populate the FinalException
property of the PolicyResult
and sets the Outcome
property to Failure
.
var retryResult = await policy.ExecuteAndCaptureAsync(() => MakeRequestEx<T(...));
if (retryResult.Outcome == Outcome.Failure)
MessageBox.Show("Operation failed after 6 attempts");
If you would like to know how many retry attempts were issued then you need to use the Context
object. The ExecuteAsync
/ExecuteAndCaptureAsync
are defined on the AsyncPolicy
base class, so they are not specific to the retry policy that's why they are not exposing the issued retry attempts.
If you define some helper methods like this:
public static class ContextExtensions
{
private static readonly string key = "RetryCount";
public static void IncreaseRetryCount(this Context context)
{
var retryCount = GetRetryCount(context);
context[key] = ++retryCount;
}
public static int GetRetryCount(this Context context)
{
context.TryGetValue(key, out object count);
return count != null ? (int)count : 0;
}
}
then you can call
- the
IncreaseRetryCount
at each retry attempt
- the
GetRetryCount
after the execution of the policy
var policy = Polly.Policy
.Handle<Exception>()
.WaitAndRetryAsync(5,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
(ex, _, ctx) =>
{
Mvx.Trace("Retried because of {0}", ex);
ctx.IncreaseRetryCount();
});
var result = await policy.ExecuteAndCaptureAsync(() => MakeRequestEx<T(...));
var outcome = result.Outcome == OutcomeType.Successful ? "completed" : "failed";
MessageBox.Show($"Operation has {outcome} after the initial attempt + {result.Context.GetRetryCount()} retry attempts");