Polly Timeout optimistic using HttpClientFactory. How can I use cancellation token?

1.1k views Asked by At

I would like to understand how to implement an optimistic timeout policy with Polly by using the HttpClientFactory.

In the examples on the net, the timeout policy is used when calling asynchronous methods to be able to pass the cancellation token. But if I set the timeout policy from configure services, as indicated in the guide), how do I manage the cancellation token?

In example code, from guide linked, I see this:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10); // Timeout for an individual try

serviceCollection.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    client.Timeout = TimeSpan.FromSeconds(60); // Overall timeout across all tries
})
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler(timeoutPolicy);
1

There are 1 answers

0
Peter Csala On

You should chain the retry and timeout policy into a combined policy.

You have two options:

Wrap method

.AddPolicyHandler(retryPolicy.Wrap(timeoutPolicy))
  • timeoutPolicy is the inner policy so it applied for each and every attempt separately
  • retryPolicy is the outer policy, so it's overarching the timeout policy
  • Note the ordering matters (I will detail it in a later section)

PolicyWrap class

.AddPolicyHandler(Policy.Wrap(retryPolicy,timeoutPolicy))
  • the first argument is the most outer policy
  • the last argument is the most inner policy

Ordering does matter

You should be aware that the following two combined policies are very different:

Policy.Wrap(retryPolicy,timeoutPolicy)
Policy.Wrap(timeoutPolicy, retryPolicy)
  • In the first case you have a local timeout, which is applied for each and every retry attempt
  • In the second case you have a global timeout, which is applied for the overall retry activities

Pushing this idea forward you can avoid to set the Timeout property of HttpClient by defining a global timeout as well:

var localTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);
var globalTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(60);
var resilientStrategy = Policy.Wrap(globalTimeoutPolicy, retryPolicy, localTimeoutPolicy);

serviceCollection.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddPolicyHandler(resilientStrategy);

UPDATE #1 Optimistic timeout

Polly's Timeout does support both optimistic and pessimistic timeouts. In other words Polly can try to cancel those to-be-wrapped methods that does anticipate a CancellationToken (optimistic) as well those methods that doesn't (pessimistic). The default is the former.

In case of optimistic you have two options:

  • Let the policy do the cancellation
await policy.ExecuteAsync(
   async ct => await httpClient.SendAsync(..., ct),
   CancellationToken.None);
  • Or combine / chain it with your custom CTS
await policy.ExecuteAsync(
   async ct => await httpClient.SendAsync(..., ct),
   cancellationSource.Token);

If you register your named/typed client during the startup then you can only use the first option. Since the policy.ExecuteAsync will be called on your behalf (implicitly).

If you register a typed client and you define the policy inside that client then you are making an explicit call of the ExecuteAsync where you can decide which version to use.