Polly timeout policy not triggered

124 views Asked by At

I'm trying to get a Polly timeout policy to execute and it's not working, but when I combine it with a retry policy it works fine. The policies are defined as follows: (Disclaimer: this is pure test code, ignore code smells)

var httpRetryPolicy = Policy.Handle<TimeoutRejectedException>()
    .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .WaitAndRetryAsync(
        5,
        retryAttempt => TimeSpan.FromSeconds(1),
        (_, _, retryAttempt, _) => Debug.WriteLine($"Retrying ({retryAttempt})")
        );

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
        4,
        TimeoutStrategy.Optimistic,
        (_, _, _, _) => { Debug.WriteLine("Timeout"); return Task.CompletedTask; }
    );

The endpoint for the policy/delay test in PollyTestController is:

[HttpGet("{policyKey}/{delay}")]
public async Task<IActionResult> RunPolicyTest(string policyKey, int delay)
{
    AsyncPolicyWrap<HttpResponseMessage> policy = null;
    if(policyKey == "T")
        policy = Policy.WrapAsync(timeoutPolicy, Policy.NoOpAsync<HttpResponseMessage>()));
    else if(policyKey == "TR")
        policy = Policy.WrapAsync(timeoutPolicy, httpRetryPolicy));

    HttpClient httpClient = new HttpClient();
    var url = $"http://localhost:5000/test/{delay}";
    try
    {
       var response = await policy.ExecuteAsync(() => httpClient.GetAsync(url));
        return Ok(await response.Content.ReadAsStringAsync());
    }
    catch (Exception ex)
    {
        return BadRequest(ex.Message); 
    }
}

And the test endpoint in TestController is:

[HttpGet("{delay}")]
public IActionResult GetWithDelay(int delay)
{
    if (delay == 10) return NotFound("Not found");

    Thread.Sleep(delay*1000);
    return Ok("Success");
}

Scenario 1 - call http://localhost:5000/pollytest/T/6

This will select the "T" policy (timeout) to execute the call to http://localhost:5000/test/6. From the code above it's clear that the response will be delayed for 6 seconds, and since the timeout in the policy is 4 seconds, a TimeoutRejectedException should be thrown. But that's not happening: The request hangs for exactly 6 seconds before returning with a "Success" response. Nothing is printed to the Output window.

Scenario 2 - call http://localhost:5000/pollytest/TR/10

This will select the "TR" policy (timeout wrapped around retry) to execute the call to http://localhost:5000/test/10. Because of the value 10 for the delay, the endpoint will return a 404, so the retry policy will try 5 times with a delay of 1 second between each try. But this time the timeout policy kicks in after the fourth try, the fifth try never happens, the Output window shows:

Retrying (1)
Retrying (2)
Retrying (3)
Retrying (4)
Timeout

and the response is "The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout."

What am I missing here? Why is Scenario 1 not triggering the timeout policy?

1

There are 1 answers

6
Peter Csala On BEST ANSWER

In case of optimistic timeout the policy uses a CancellationToken to stop the decorated method if the predefined duration is elapsed.

So, all you need is to pass use a different overload of the ExecuteAsync

var response = await policy.ExecuteAsync(ct => httpClient.GetAsync(url, ct), CancellationToken.None);
  • The second parameter of the ExecuteAsync is a user-defined cancellation token. If you have your own you can provide it here and Polly will chain that to the Timeout's CancellationToken. If you don't have then you can provide None.
  • The ct is the Timeout's CancellationToken which either a chained or a single token depending on the second parameter of the ExecuteAsync

UPDATE #1

Let's focus on this bit:

AsyncPolicyWrap<HttpResponseMessage> policy = null;
if(policyKey == "T")
    policy = Policy.WrapAsync(timeoutPolicy, Policy.NoOpAsync<HttpResponseMessage>()));
else if(policyKey == "TR")
    policy = Policy.WrapAsync(timeoutPolicy, httpRetryPolicy);

In case of "T" you have used the NoOp. That policy was designed for unit testing only: test your method like there is no policy. Having the NoOp as the inner policy has the same effect as using the NoOp directly, it will not trigger escalation:

var noop = Policy.NoOpAsync<HttpResponseMessage>();
var retry = Policy<HttpResponseMessage>.HandleResult(r => !r.IsSuccessStatusCode).RetryAsync(3);
var wrap = Policy.WrapAsync(retry, noop);

var res = await wrap.ExecuteAsync(() =>  {
    Console.WriteLine("Called");
    return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent));
});

Output

Called

So, you have to use only the timeoutPolicy if you receive "T". To do that, you have to change the policy's type from AsyncPolicyWrap<HttpReponseMessage> to simply IAsyncPolicy<HttpResponseMessage>

IAsyncPolicy<HttpResponseMessage> policy = null;
if(policyKey == "T")
    policy = timeoutPolicy;
else if(policyKey == "TR")
    policy = Policy.WrapAsync(timeoutPolicy, httpRetryPolicy);

Or

IAsyncPolicy<HttpResponseMessage> policy = timeoutPolicy;
if(policyKey == "TR")
    policy = Policy.WrapAsync(timeoutPolicy, httpRetryPolicy);