-1

I am using Polly to make HTTP requests and retry 5 attempts if the request fails.

Is it possible to specify an action when 5 attempts have failed and the policy gives up?

In the below code; 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'. I can use a counter to count 5 fails but it would be nicer to use a Polly method.

var policy = Polly.Policy.Handle<Exception>().WaitAndRetryAsync(
   5,
   retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
   (ex, span) =>
   {
       Mvx.Trace("Retried because of {0}", ex);
   }
);

await policy.ExecuteAsync(() => MakeRequestEx<T>(requestUrl, verb, accept, headers, baseAddress)).ConfigureAwait(false);
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
sazr
  • 24,984
  • 66
  • 194
  • 362

2 Answers2

1

Yes, you can use ExecuteAndCapture

var policyResult = await policy.ExecuteAndCaptureAsync(
    () => MakeRequestEx<T>(requestUrl, verb, accept, headers, baseAddress)
).ConfigureAwait(false);

You can then inspect the Outcome of the policyResult to check whether the call failed and display a message.

see https://github.com/michael-wolfenden/Polly#post-execution-steps for more information.

Michael Wolfenden
  • 2,911
  • 1
  • 13
  • 7
0

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");
Peter Csala
  • 17,736
  • 16
  • 35
  • 75