2

I am using a Polly retry policy and as expected during that retry processes the HttpClient hits its 100 second timeout. I have tried a couple of different ways to incorporate the Polly Timeout policy to move the timeout to per retry instead of total, but the 100 second timeout keeps firing.

I have read about 5 StackOverflow questions that all say to wrap the policies, and I even found the demo on the Polly wiki that say to call AddPolicyHandler twice. That did not work either. I am sure I have some very basic wrong. Please explain to me the error of my ways.

I am using .net core 5 preview 7 and current Polly nuget packages.

Typed HttpClient registration

serviceCollection.AddHttpClient<APIWrappers.SendGridClientEmail, APIWrappers.SendGridClientEmail>((c) =>
{
    c.BaseAddress = new Uri("https://api.sendgrid.com/v3a/");
})
    .AddPolicyHandler((services, req) => TransientLogAndRetry<APIWrappers.SendGridClientEmail>(services, req)
    .WrapAsync(Policy.TimeoutAsync<HttpResponseMessage>(10)));

The Policy definition

public IAsyncPolicy<HttpResponseMessage> TransientLogAndRetry<T>(IServiceProvider services, object req)
{
    Console.WriteLine($"Name: {typeof(T).Name} - Time: {DateTime.Now}");
    return HttpPolicyExtensions.HandleTransientHttpError().
        OrResult((rsp) => rsp.StatusCode == System.Net.HttpStatusCode.TooManyRequests).
        Or<Polly.Timeout.TimeoutRejectedException>().
        WaitAndRetryAsync(5, GetRetryTime,
#pragma warning disable CS1998
            async (ex, ts, rc, ctx) =>
            {
                ((STS.Diagnostics.LoggerFactory)services.GetService<ILoggerFactory>())?.CreateLogger<T>().
                    Warn(this, "HTTP request above will retry in {0}", ts);
            }
#pragma warning restore CS1998
        );
}

The delay / penalty calculation

private TimeSpan GetRetryTime(int retryCount, DelegateResult<HttpResponseMessage> response, Context context)
{
    TimeSpan result;
    long unix;

    Console.WriteLine($"Count: {retryCount} - Time: {DateTime.Now}");
    if (response.Result is null || response.Result.StatusCode != System.Net.HttpStatusCode.TooManyRequests)
        result = TimeSpan.FromSeconds(Math.Min(Math.Pow(Math.Sqrt(retryCount * 2), 5), 60 * 60));
    else
    {
        unix = response.Result.Headers.GetValues("X-RateLimit-Reset").Select((v) => long.Parse(v)).First();
        result = DateTime.UtcNow - DateTimeOffset.FromUnixTimeSeconds(unix);
    }
    Console.WriteLine(result);
    return result;
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
SOHO Developer
  • 583
  • 4
  • 16

1 Answers1

3

Based on the provided code I can see the following resilient strategy:

100 seconds global timeout

5 retries (= 5+1 attempts)

Exponential back-off logic

10 seconds local timeout


If I understand your problem correctly then you are looking for a solution to increase the global timeout.

The simplest solution (1) is to increase the HttpClient instance's Timeout property from the default 100 to the desired value. If this timeout is exceeded then the instance will throw an OperationCanceledException and no further retry attempt can be established.

If the HttpClient instance is shared / reused amongst different components then you might need to change the global timeout per usage. You can overcome on this by introducing a new Timeout policy between the HttpClient's global Timeout and your Retry policy.

So, you could end up with the following setup:

200 seconds global timeout (by HttpClient

150 seconds overall / overarching timeout (by outer timeout policy)

5 retries (= 5+1 attempts)

Exponential back-off logic

10 seconds local timeout

In this particular case I would recommend to use Policy.WrapAsync (1) just like this:

Policy.WrapAsync(OverallTimeoutPolicy(), RetryPolicy(), LocalTimeoutPolicy());
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • No, I do not want to increase the global timeout, I want the timeout to be by retry. If one sets the global time out to 300 seconds (to cover the retries), the first attempt will not timeout for 5 minutes. – SOHO Developer Aug 25 '20 at 19:19
  • @SOHODeveloper Do I understand correctly that your 10 seconds local timeout has no effect? – Peter Csala Aug 26 '20 at 06:01
  • It may have effect. I have not hit it. The problem is that the 100 hits during the 3 retry and I never get to the rest of the retries. I have tested this by forcing what looks like a transient error. My understanding of the code and comments that I read is that the retry code should 'reset' the global timeout in some way. – SOHO Developer Aug 27 '20 at 09:17
  • @SOHODeveloper No that's not true. Retry resets only the local timeout (per attempt). From the HttpClient perspective it looks like a single request (all attempts together). It might worth to introduce a **Circuit Breaker** to avoid over-flooding the downstream system. It could open after three successive failed attempts. – Peter Csala Aug 27 '20 at 09:59
  • 1
    So I should set my global timeout to large number to cover all my retries in wall clock time including the wait time between retries. You suggested another policy wrap above. I have not seen a usage like that in the examples or documentation. Do I need it? If so please add some explanation for me and future people on why it too is needed. – SOHO Developer Aug 28 '20 at 10:11
  • @SOHODeveloper Please check Reisenberger's (architect of Polly project) answer of [this issue](https://github.com/App-vNext/Polly/issues/512). He is a more authentic source than me :D – Peter Csala Aug 28 '20 at 13:28
  • @SOHODeveloper Is it sufficient information or should I elaborate? – Peter Csala Sep 02 '20 at 15:51
  • 1
    Thank you for all your discussion. I had already read the information in the issue you referenced. On about the 3 or 4 reading I picked up a word or two that I had missed before and our discussion had me thinking in a different direction, so I interpreted a couple of things differently and I believe I know how to progress. I am currently not working on this issue, so it will be awhile before I know for sure. I am marking answered because you stuck with me. Thanks. – SOHO Developer Sep 02 '20 at 18:35
  • I am wondering when the `200` seconds timeout of the `HttpClient` would reach in this setup because it seems the `150` sec timeout would trigger earlier. – Dr. Strangelove Sep 13 '22 at 14:13
  • 1
    @Dr.Strangelove No, that timeout won't be triggered since the global timeout policy fires and then throws a TimeoutRejectedException. – Peter Csala Sep 13 '22 at 15:49