DotNetCore 中的 HttpClient.SendAsync - 是否可能存在死锁?

HttpClient.SendAsync in DotNetCore - Is a Deadlock Possible?

提问人:Stuart 提问时间:3/24/2021 最后编辑:Stuart 更新时间:3/24/2021 访问量:362

问:

根据我们的日志,我们偶尔会接到一个呼叫,该呼叫在我们为请求配置的超时内完成。第一个日志条目来自服务器。这是该方法在返回 JsonResult(MVC 4 控制器)之前注销的最后一件事。TaskCanceledException

{
    "TimeGenerated": "2021-03-19T12:08:48.882Z",
    "CorrelationId": "b1568096-fdbd-46a7-8b69-58d0b33f458c",
    "date_s": "2021-03-19",
    "time_s": "07:08:37.9582",
    "callsite_s": "...ImportDatasets",
    "stacktrace_s": "",
    "Level": "INFO",
    "class_s": "...ReportConfigController",
    "Message": "Some uninteresting message",
    "exception_s": ""
}

在本例中,请求大约需要 5 分钟才能完成。然后 30 分钟后,我们的调用方抛出任务取消异常:HttpClient.SendAsync

{
    "TimeGenerated": "2021-03-19T12:48:27.783Z",
    "CorrelationId": "b1568096-fdbd-46a7-8b69-58d0b33f458c",
    "date_s": "2021-03-19",
    "time_s": "12:48:17.5354",
    "callsite_s": "...AuthorizedApiAccessor+<CallApi>d__29.MoveNext",
    "stacktrace_s": "TaskCanceledException    
        at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   
        at System.Net.Http.HttpConnectionPool.SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)\r\n   
        at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)\r\n   
        at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   
        at System.Net.Http.DecompressionHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   
        at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)\r\n   
        at ...AuthorizedApiAccessor.CallApi(String url, Object content, HttpMethod httpMethod, AuthenticationType authType, Boolean isCompressed)\r\nIOException    
        at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)\r\n   
        at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)\r\n   
        at System.Net.Security.SslStream.<FillBufferAsync>g__InternalFillBufferAsync|215_0[TReadAdapter](TReadAdapter adap, ValueTask`1 task, Int32 min, Int32 initial)\r\n   
        at System.Net.Security.SslStream.ReadAsyncInternal[TReadAdapter](TReadAdapter adapter, Memory`1 buffer)\r\n   
        at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)\r\nSocketException",
    "Level": "ERROR",
    "class_s": "...AuthorizedApiAccessor",
    "Message": "Nothing good",
    "exception_s": "The operation was canceled."
}

鉴于在发出请求的过程中,我们阻止了异步调用( -- 击中不支持异步的棕地缓存实现),我的第一个猜测是 Stephen Cleary 所描述的我们遇到了死锁。但是调用方是 dotnetcore 3.1 应用程序,因此这种死锁是不可能的。.Result

我认为我们的用法是相当标准的。这是最终进行调用的方法:HttpClient

private async Task<string> CallApi(string url, object content, HttpMethod httpMethod, AuthenticationType authType, bool isCompressed)
{
    try
    {
        var request = new HttpRequestMessage()
        {
            RequestUri = new Uri(url),
            Method = httpMethod,
            Content = GetContent(content, isCompressed)
        };

        AddRequestHeaders(request);

        var httpClient = _httpClientFactory.CreateClient(HTTPCLIENT_NAME);
        httpClient.Timeout = Timeout;

        AddAuthenticationHeaders(httpClient, authType);

        var resp = await httpClient.SendAsync(request);
        var responseString = await (resp.Content?.ReadAsStringAsync() ?? Task.FromResult<string>(string.Empty));

        if (!resp.IsSuccessStatusCode)
        {
            var message = $"{url}: {httpMethod}: {authType}: {isCompressed}: {responseString}";
            if (resp.StatusCode == HttpStatusCode.Forbidden || resp.StatusCode == HttpStatusCode.Unauthorized)
            {
                throw new CustomException(message, ErrorType.AccessViolation);
            }

            if (resp.StatusCode == HttpStatusCode.NotFound)
            {
                throw new CustomException(message, ErrorType.NotFound);
            }

            throw new CustomException(message);
        }

        return responseString;
    }
    catch (CustomException) { throw; }
    catch (Exception ex)
    {
        var message = "{Url}: {HttpVerb}: {AuthType}: {IsCompressed}: {Message}";
        _logger.ErrorFormat(message, ex, url, httpMethod, authType, isCompressed, ex.Message);
        throw;
    }
}

我们对这种行为的理论感到茫然。我们已经看到,在几百个成功的请求中,每个月有 3-5 次任务被取消,所以它是间歇性的,但远非罕见。

我们还应该在哪里寻找死锁的根源?

更新

可能需要注意的是,我们正在使用标准。最近添加了重试策略,但我们不会在长时间运行的 POST 上重试,这是上面的场景。HttpClientHandler

builder.Services.AddHttpClient(AuthorizedApiAccessor.HTTPCLIENT_NAME)
    .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
    {
        AutomaticDecompression = System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip
    })
    .AddRetryPolicies(retryOptions);
ASP.NET-Core HttpClient sendasync

评论

1赞 Stephen Cleary 3/24/2021
这不是死锁,因为调用已完成(稍后,将记录异常)。有趣的是,就在昨晚,我还首次在使用 HttpClient 的 .NET Core Azure 函数中观察到了这种行为。调用完成,然后一段时间后,HttpClient 引发具有相同调用 ID 的取消异常。我不完全确定这怎么可能。
0赞 Stuart 3/24/2021
是的,我不知道有更好的作品来描述这种行为。也许“挂起”更好——(正确地)暗示我们不知道发生了什么。

答: 暂无答案