当从 Web API 项目调用时,Polly 在 .NET Framework 4.6.1 上无限期地等待线程

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

.NET 版本:.NET Framework 4.6.1
波莉版本:7.2.2

在 .NET Framework 4.6.1 上,当使用 Web API 项目时,Polly 将无限期地等待请求所在的线程 运行,导致永远不会有响应返回给调用它的客户端。从控制台应用程序调用相同的方法就可以正常工作。

这是使用 Visual Studio 'ASP.NET Web Application (.NET Framework)' 中新创建的解决方案进行测试的。 我也在 .NET 5 中尝试了同样的代码,但这个问题不存在,它只发生在 .NET Framework 4.6.1 上。

重现问题的代码

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);
        }
    }
}

这是你的错误:

var response = PollyTestRequest.Send().Result;

Don't block on async code;在像 ASP.NET (pre-Core) 这样的情况下,它可能会导致死锁。

正确的解决方法是使用 async all the way:

public async Task<IHttpActionResult> TestGetRoute()
{
  var response = await PollyTestRequest.Send();
  ...
}