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)
.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.