Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polly waits the thread for an indefinite amount of time on .NET Framework 4.6.1 when called from a Web API project #868

Closed
CorruptComputer opened this issue Jun 9, 2021 · 2 comments
Labels
external Issue is outside the scope of this project

Comments

@CorruptComputer
Copy link

Summary:

.NET Version: .NET Framework 4.6.1
Polly Version: 7.2.2

On .NET Framework 4.6.1 when using a Web API project Polly will wait the thread the request is running in for an indefinite amount of time, causing there to never be a response back to the client that called it. Calling the same method from a console app will work just fine.

This was tested using a freshly created solution in Visual Studio 'ASP.NET Web Application (.NET Framework)'.
I also tried this same code in .NET 5 and this issue is not present, it only happens on .NET Framework 4.6.1.


Expected behavior:

Polly policies should be executed without hanging the request.


Actual behaviour:

The request will become hung, and the client that called the API will never get a response.


Steps / Code to reproduce the problem:

PolicyContainer.cs:

public class PolicyContainer
{
    public IAsyncPolicy<HttpResponseMessage> CircutBreakerPolicy { get; set; }

    public PolicyContainer()
    {
        SetCircutBreakerPolicy();
    }

    private void SetCircutBreakerPolicy()
    {
        //////////////////////////////////////////////////////////////////////////////////////
        // Normally these values would be set by a config file, hardcoded for this example. //
        //////////////////////////////////////////////////////////////////////////////////////
        // 0.5 means 50% of requests must fail before the circut breaks
        double failureThreshold = 0.5;

        // 60 means only the most recent 60 seconds are considered for breaking the circut
        double samplingDuration = 60;

        // 10 means at least this many calls must pass through the circut within the samplingDuration before breaking the circut
        int minimumThroughput = 10;

        // 60 means the circut will be broken for 60 seconds after the threshold is met
        double durationOfBreak = 60;
            
            
        CircutBreakerPolicy = Policy.HandleResult<HttpResponseMessage>(result => !result.IsSuccessStatusCode)
            .AdvancedCircuitBreakerAsync(failureThreshold,
                                            TimeSpan.FromSeconds(samplingDuration),
                                            minimumThroughput,
                                            TimeSpan.FromSeconds(durationOfBreak),
                                            OnBreak,
                                            OnReset,
                                            OnHalfOpen);
    }

    private void OnBreak(DelegateResult<HttpResponseMessage> response, TimeSpan timespan, Context context)
    {
        Console.WriteLine("Circut Broken");
    }

    private void OnReset(Context context)
    {
        Console.WriteLine("Circut Reset");
    }

    private void OnHalfOpen()
    {
        Console.WriteLine("Circut Half-Open");
    }
}

PollyTestRequest.cs:

public class PollyTestRequest
{
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////
    // If set to true the Web API will never return a response, though any other type of project works fine. //
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////
    private const bool USE_POLLY = true;

    public static async Task<HttpResponseMessage> Send()
    {
        HttpClient httpClient = new HttpClient();
        PolicyContainer policyContainer = new PolicyContainer();
        HttpResponseMessage response;

        if (USE_POLLY)
        {
            // Does not work in a Web API application. 
            // I stepped through the decompiled code this calls and it will arrive at a "public static bool Wait(object obj, int millisecondsTimeout, bool exitContext)" method.
            // Inside this method there is a call to "ObjWait(exitContext, millisecondsTimeout, obj)", however the debugger will not decompile this method so the debugging session will stop if you try to step into it.
            // The 'millisecondsTimeout' variable passed here will be "-1" and the 'exitContext' will be "null". I believe that this is what is hanging the thread indefinitely.
            // Its very strange though, calling this from a Console app, it will work fine, but from a Web API application it will hang indefinitely.
            response = await policyContainer.CircutBreakerPolicy.ExecuteAsync(
                            async token => await httpClient.PostAsync(new Uri("http://example.com"), new StringContent(""), token), 
                            CancellationToken.None
                        );
        }
        else
        {
            // Works perfectly fine in both Web API and Console Apps
            response = await httpClient.PostAsync(new Uri("http://example.com"), new StringContent("")).ConfigureAwait(false);
        }

        return response;
    }
}

TestController.cs:

[Route("[controller]")]
public class TestController : ApiController
{
    [HttpGet]
    [Route("testRoute")]
    public IHttpActionResult TestGetRoute()
    {
        var response = PollyTestRequest.Send().Result;

        if (response.IsSuccessStatusCode)
        {
            return Ok();
        }
        else
        {
            return new StatusCodeResult(HttpStatusCode.InternalServerError, this);
        }
    }
}
@martincostello
Copy link
Member

You're doing .Result on a Task from the ASP.NET controller.

This is a well-known way to create a deadlock when running under IIS in .NET Framework due to the way the threading model/synchronisation context works.

Change the controller method to be async and await the call, I'm pretty sure it will resolve the deadlock.

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

@CorruptComputer
Copy link
Author

Solved!

In TestController.cs I just changed the following line:

var response = PollyTestRequest.Send().Result;

To:

var response = Task.Run(async () => await PollyTestRequest.Send()).Result;

Thanks for your help!

@martincostello martincostello added the external Issue is outside the scope of this project label Jun 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
external Issue is outside the scope of this project
Projects
None yet
Development

No branches or pull requests

2 participants