3

I'm using Polly (Microsoft.Extensions.Http.Polly) with .net core with this configuration (with an invalid URL , for testing) :

private static void RegisterServices()
{
    var collection = new ServiceCollection();
    var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(2); // Timeout for an individual try
 

    collection.AddHttpClient<INetworkService, NetworkService>(url=>
             {
                 url.BaseAddress = new Uri("http://www.google.com:81"); //test bad url
             })
             .AddPolicyHandler(GetRetryPolicy()) 
             .AddPolicyHandler(timeoutPolicy); ;

    _serviceProvider = collection.BuildServiceProvider();
}

Where GetRetryPolicy is :

private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode != HttpStatusCode.OK)
        .Or<TimeoutRejectedException>()
        .Or<TaskCanceledException>()
        .Or<OperationCanceledException>()
        .WaitAndRetryAsync(3, retryAttempt =>
        {
        return  TimeSpan.FromSeconds(2);
        }, 
        onRetry: (response, delay, retryCount, context) =>
            {
              Console.WriteLine($"______PollyAttempt_____ retryCount:{retryCount}  ");
            });
}

Output is :

_PollyAttempt retryCount:1
_PollyAttempt retryCount:2
_PollyAttempt retryCount:3
Exception : (TimeoutException) The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.

I want to send an email after the last attempt was failing.

Question:

How can I catch that final exception? Is there any built-in mechanism that allows me to know that Polly was failed ?

(my currently working code : https://pastebin.pl/view/a2566d51)

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Royi Namir
  • 144,742
  • 138
  • 468
  • 792
  • The retry policy will propagate the original exception if all the retry attempts failed. If you call the `ExecuteAndCapture` rather than the `Execute` then you can manually examine the exception. – Peter Csala Apr 16 '21 at 11:58
  • Are you also sure that the retry is the inner policy? (Usually I prefer a single `AddPolicyHandler` where I pass a `Policy.WrapAsync( ...)`) I'm not sure how they are registered (in which order) when you are issuing multiple `AddPolicyHandler`s – Peter Csala Apr 16 '21 at 12:00
  • @PeterCsala Thanks for reply However it seems that I can't use executeAndCApture becuase it doesn't compile.I;ve put my code in [here](https://pastebin.pl/view/a2566d51) – Royi Namir Apr 16 '21 at 12:02
  • What sort of compilation error do you observe? – Peter Csala Apr 16 '21 at 12:06
  • @PeterCsala i just wanted it to compile : but i get this https://i.imgur.com/0tcmIgO.jpg . All I want is to catch the last exception with this code that I have ( which is from MS btw where they use - https://i.imgur.com/xet3n1c.jpg – Royi Namir Apr 16 '21 at 12:14
  • Yes it won't compile in this way. There is a policy declaration which returns an `IAsyncPolicy` and policy execution which returns a `PolicyResult` – Peter Csala Apr 16 '21 at 12:19
  • @PeterCsala So I can't catch final excpetion using MS's code ? – Royi Namir Apr 16 '21 at 12:20
  • With Typed HttpClient you can't because the policy itself is hidden from us. Later in the afternoon I will try to put together a sample – Peter Csala Apr 16 '21 at 12:21
  • On the other hand there is an `onRetry` overload which receives the causing exception if any. – Peter Csala Apr 16 '21 at 12:23
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/231209/discussion-between-royi-namir-and-peter-csala). – Royi Namir Apr 16 '21 at 12:23

1 Answers1

4

Let's start with a simple setup where you don't use Polly at all:

private static void RegisterServices()
{
    var collection = new ServiceCollection();
    collection.AddHttpClient<INetworkService, NetworkService>(sonol =>
    {
        sonol.BaseAddress = new Uri("http://www.google.com:81");
    });

    _serviceProvider = collection.BuildServiceProvider();
}
  • After 100 seconds (the default timeout of HttpClient) it will fail with a TaskCanceledException. In other words HttpClient cancels the request because it did not receive any response.

Now let's bend the HttpClient setup a bit:

private static void RegisterServices()
{
    var collection = new ServiceCollection();
    collection.AddHttpClient<INetworkService, NetworkService>(sonol =>
    {
        sonol.Timeout = TimeSpan.FromSeconds(3); // << NEW CODE
        sonol.BaseAddress = new Uri("http://www.google.com:81");
    });

    _serviceProvider = collection.BuildServiceProvider();
}
  • After 3 seconds the HttpClient cancels the request and throws a TaskCanceledException

Now, comment out this timeout setup and let's wire up the timeout Policy:

private static void RegisterServices()
{
    var collection = new ServiceCollection();
    var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(2);

    collection.AddHttpClient<INetworkService, NetworkService>(sonol =>
    {
        //sonol.Timeout = TimeSpan.FromSeconds(3);
        sonol.BaseAddress = new Uri("http://www.google.com:81");
    })
    .AddPolicyHandler(timeoutPolicy); // << NEW CODE

    _serviceProvider = collection.BuildServiceProvider();
}
  • After 2 seconds Polly's TimeoutPolicy cancels the request and throws a TimeoutRejectedException.
    • Its InnerException is the original TaskCanceledException.

And finally let's add retry policy:

private static void RegisterServices()
{
    var collection = new ServiceCollection();
    var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(2);

    collection.AddHttpClient<INetworkService, NetworkService>(sonol =>
    {
        //sonol.Timeout = TimeSpan.FromSeconds(3);
        sonol.BaseAddress = new Uri("http://www.google.com:81");
    })
    .AddPolicyHandler(Policy.WrapAsync(GetRetryPolicy(), timeoutPolicy)); // << NEW CODE
    //.AddPolicyHandler(timeoutPolicy);

    _serviceProvider = collection.BuildServiceProvider();
}
  • After 14 seconds (3+1 2 seconds long requests & 3 2 seconds long penalties) the retry policy throws the original exception, which is the innerPolicy's TimeoutRejectedException.
    • That exception's Inner is the HttpClient's TaskCanceledException.

UPDATE: Capture the essence of the comments

Where is the point where I know that all attempts have failed?

When your Polly decorated HttpClient throws the TimeoutRejectedException, you can be sure that all attempts failed. So, you should wrap the GetAsync with try-catch inside the typed client.

Should I check the exception to see that it's timeoutException?

In case of malformed url it will throw different exception. So, if you catch TimeoutRejectedException that would mean either the downstream is not available or it is overloaded.

Do I need to catch first TimeoutRejectedException exception in order to recognize retires have failed?

From the consumer perspective there will be a single exception. The retry policy will throw it

  • when it runs out of the retry attempts
  • or when it is not configured to handle it.
    • All those exceptions that are not listed explicitly via Handle<> or Or<> calls they are treated as unhandled. This means without any retry the policy will throw that.

In other words, TimeoutRejectedException will be thrown if the client will not receive answer from the downstream system for a given time period. But it might also throw HttpRequestException if there is some network issue.

  • If the retry is configure to handle that then you can be sure that if it is thrown then all retry attempts failed.
  • If it is not configured then without any retry it will throw the HttpRequestException.
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • Appreciated. But where is the point where i know that all attempts have failed and send an email saying that polly has failed all retry attempts? – Royi Namir Apr 16 '21 at 15:23
  • Question was :`I want to send an email after the last attempt was failing.` ? meaning the last exception – Royi Namir Apr 16 '21 at 15:39
  • @RoyiNamir When your polly decorated httpClient throws the TimeoutRejectedException, you can be sure that all attempts failed. – Peter Csala Apr 16 '21 at 15:43
  • 1
    but where do I **catch** it ? – Royi Namir Apr 16 '21 at 15:44
  • Inside the typed client. You should wrap the GetAsync with try-catch. – Peter Csala Apr 16 '21 at 15:45
  • do u mean wrap the HttpCall with try/catch ? – Royi Namir Apr 16 '21 at 15:45
  • Yes, the GetAsync. – Peter Csala Apr 16 '21 at 15:47
  • 1
    OMG you're right : https://i.imgur.com/QGhntDa.jpg . Should I check the exception to see that it's timeoutException ? – Royi Namir Apr 16 '21 at 15:47
  • In case of malformed url it will throw different exception. So if you catch TimeoutRejectedException that would mean either the downstream is not available or it is overloaded. – Peter Csala Apr 16 '21 at 15:51
  • Sorry , jsut to see if i get you , I do need to catch first TimeoutRejectedException exception in order to recognize retires have failed. right ? – Royi Namir Apr 16 '21 at 15:57
  • @RoyiNamir From the consumer perspective there will be only 1 exception. The retry policy will throw it when it runs out of the retry attempts or it is not configured to handle it. – Peter Csala Apr 16 '21 at 16:07
  • All those exceptions that are not listed explicitly in the Handle or in the Or calls they are treated as unhandled. Which means without any retry the policy will throw that. – Peter Csala Apr 16 '21 at 16:09
  • I know that there will be only one exception. But if i want to he sure that all retries have failed , i must first catch timedoutexception. Am I correct? – Royi Namir Apr 16 '21 at 16:09
  • As I said before TimeoutRejectedException will be thrown if the client will not receive answer from the downstream system at a given time. But it might also throw HttpRequestException if there is some network issue. If the retry is configure to handle that then you can be sure that if it is thrown then all retry attempts failed. If not configure then without retry it will throw the HttpRequestException. – Peter Csala Apr 16 '21 at 16:15