3
votes

I'm trying to implement a Polly Timeout policy using the new .NET Core 2.1 HttpClientFactory; however, I cannot seem to get the timeout to occur.

My ConfigureServices:

// Configure polly policies
TimeoutPolicy<HttpResponseMessage> timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(5, TimeoutStrategy.Pessimistic);

// Configure platform service clients
services.AddHttpClient<IDiscoveryClient, DiscoveryClient>()
    .AddPolicyHandler(timeoutPolicy);

My POST method in DiscoveryClient:

public async Task<TResponse> PostXMLAsync<TResponse, TPostData>(string url, TPostData postData)
    where TResponse : ClientResponse
    where TPostData : ClientPostData
{
    HttpResponseMessage response = await httpClient.PostAsXmlAsync(url, postData);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsAsync<TResponse>();
}

Unfortunately, the call times out after the default 100s rather than after the 5s defined in the Polly policy.

Any thoughts on what I'm doing wrong?

1
Where are you seeing the timeout? At await httpClient.PostAsXmlAsync(url, postData) or at await response.Content.ReadAsAsync<TResponse>()? How are you seeing the timeout? In logs, or are you seeing a timeout thrown as an exception from either of those two code lines?mountain traveller
Essentially, the 5s timeout is not happening. Instead, await httpClient.PostAsXmlAsync(url, postData) is taking 100s to complete and then I receive a HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error). from response.EnsureSuccessStatusCode(); The service I'm calling is known to do this from time to time which is why I wanted to implement the Polly Timeout policy.wdspider
Can you confirm what happens with TimeoutStrategy.Optimistic? HttpClientFactory definitely transmits the cancellationtoken downstream, so I would have expected TimeoutStrategy.Optimistic to be adequate. Otherwise we will need to construct a minimal reproducible example .mountain traveller
TimeoutStrategy.Optimistic doesn't seem to matter. I created a demo here: gist.github.com/wdspider/a1cf8328dbcf6cd42ea0a889f5427f0b The slow responding web service that I'm calling is not public; therefore, I simply added a 20s delay in the ClientLoggingHandler which should be a decent simulation.wdspider
The timeout in gist.github.com/wdspider/a1cf8328dbcf6cd42ea0a889f5427f0b fails because the order of delegating handlers is such that the await Task.Delay(...) executes before (is 'outside', in terms of nesting) the TimeoutPolicy; ie the timeout policy doesn't govern the delay. Reverse it to services....AddPolicyHandler(timeoutPolicy).AddHttpMessageHandler<ClientLoggingHandler>(); and the timeout works. Does this info help the original case?mountain traveller

1 Answers

0
votes

First let's define a mock server which does respond with 500 after 100 seconds:

const string address = "http://localhost:9000";
var delay = TimeSpan.FromSeconds(100);
var server = WireMockServer.Start(new WireMockServerSettings { Urls = new[] { address } });
server
    .Given(Request.Create().WithPath("/").UsingPost())
    .RespondWith(Response.Create().WithDelay(delay).WithStatusCode(500));

I've used WireMock.Net for this.

Now, let's see the IDiscoveryClient and DiscoveryClient:

interface IDiscoveryClient
{
    Task<TResponse> SendRequest<TResponse, TPostData>(string url, TPostData data);
}
class DiscoveryClient : IDiscoveryClient
{
    private readonly HttpClient httpClient;

    public DiscoveryClient(HttpClient httpClient) => this.httpClient = httpClient;

    public async Task<TResponse> SendRequest<TResponse, TPostData>(string url, TPostData data)
    {
        var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8);
        var response = await httpClient.PostAsync(url, content);
        response.EnsureSuccessStatusCode();
        var rawData = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<TResponse>(rawData);
    }
}
class TestRequest { public string Content { get; set; } }
class TestResponse { public string Data { get; set; } }

I've used json instead of xml, but that's not imporant from the question point of view.

And finally let's wire up the DI and issue a request:

AsyncTimeoutPolicy<HttpResponseMessage> timeoutPolicy =
    Policy.TimeoutAsync<HttpResponseMessage>(5, TimeoutStrategy.Pessimistic);

IServiceCollection services = new ServiceCollection();
services.AddHttpClient<IDiscoveryClient, DiscoveryClient>()
    .AddPolicyHandler(timeoutPolicy);

ServiceProvider serviceProvider = services.BuildServiceProvider();
var client = serviceProvider.GetService<IDiscoveryClient>();

Stopwatch sw = Stopwatch.StartNew();
try
{
    TestResponse res = await client.SendRequest<TestResponse, TestRequest>(address, new TestRequest { Content =  "Test"});
}
catch (TimeoutRejectedException ex)
{
    sw.Stop();
    Console.WriteLine(sw.Elapsed);
}

The printed output will be something like this:

00:00:05.0296804

The good thing is that it does work with Optimistic and Pessimistic strategies as well.