【发布时间】:2021-03-24 21:25:09
【问题描述】:
在我的 C# 应用程序 (.NET Core 3.1) 中,有一个自动任务,每隔 X 小时启动另一个任务,该任务使用不同的参数并行运行多次。
在这个自动任务结束时,会调用 await Task.WhenAll(tasksList). 以等待并行任务完成。
每个任务都会发出一个 HTTPClient(使用 IHttpClientFactory 工厂方法)并发出一个 GET 请求,语法如下:
var res = await client.GetAsync(url);
if (res.IsSuccessStatusCode)
{
var exit = await res.Content.ReadAsStringAsync();
[...omitted]
}
当共享相同 GET URL 的两个任务在最长 60-70 毫秒的距离上运行时,问题随机发生。有时两个任务一个接一个地失败,每个任务都有同样的例外:
System.Net.Http.HttpRequestException: Error while copying content to a stream.
---> System.IO.IOException: The response ended prematurely.
at System.Net.Http.HttpConnection.FillAsync()
at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionResponseContent.SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
从日志中,我可以看到服务器如何正确启动和接收两个不同的 HTTP 请求。
如果我删除 ReadAsStringAsync 部分,问题永远不会发生,所以我认为它与 内容读取(在状态码检查之后)有关,几乎就像两者任务结束共享 Get 结果(同时发出两个不同的活动连接)。我尝试使用 ReadAsStreamAsync,但问题仍然存在(不过,这有助于减少发生率)。
另一件可能相关的事情是检索到的结果非常重(我上次下载它时,它最终是一个 4.5MB 的 .json 文件,或多或少)。
我应该按顺序运行每个任务吗?还是我发出的 HTTP 请求有误?
如果您想测试此问题,您可以在此处找到我用来重现该问题的控制台应用程序的源代码(如果前 20 次调用未发生,请重新启动应用程序,直到它发生):
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static async Task<int> Main(string[] args)
{
var builder = new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddHttpClient();
services.AddTransient<TaskRunner>();
}).UseConsoleLifetime();
var host = builder.Build();
using (var serviceScope = host.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
try
{
var x = services.GetRequiredService<TaskRunner>();
var result = await x.Run();
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine("Error Occured");
}
}
return 0;
}
public class TaskRunner
{
private static IHttpClientFactory _httpFactory { get; set; }
public TaskRunner(IHttpClientFactory httpFactory)
{
_httpFactory = httpFactory;
}
public async Task<string> Run()
{
Console.WriteLine("Starting loop...");
do
{
await Task.Delay(2500); // wait app loading
await SendRequest();
} while (true);
}
private static async Task SendRequest()
{
await Task.WhenAll(new Task[] { ExecuteCall(), ExecuteCall()};
}
private async static Task<bool> ExecuteCall()
{
try
{
var client = _httpFactory.CreateClient();
// fake heavy API call (> 5MB data)
var api = "https://api.npoint.io/5896085b486eed6483ce";
Console.WriteLine("Starting call at " + DateTime.Now.ToUniversalTime().ToString("o"));
var res = await client.GetAsync(api);
if (res.IsSuccessStatusCode)
{
var exit = await res.Content.ReadAsStringAsync();
/* STREAM read alternative
var ed = await res.Content.ReadAsStreamAsync();
StringBuilder result = new StringBuilder();
using var sr = new StreamReader(ed);
while (!sr.EndOfStream)
{
result.Append(await sr.ReadLineAsync());
}
var exit = result.ToString();
*/
Console.WriteLine(exit.Substring(0, 10));
//Console.WriteLine(exit);
Console.WriteLine("Ending call at " + DateTime.Now.ToUniversalTime().ToString("o"));
return true;
}
Console.WriteLine(res.StatusCode);
Console.WriteLine("Ending call at " + DateTime.Now.ToUniversalTime().ToString("o"));
return false;
}
catch (Exception ex)
{
// put breakpoint here
// Exception => called on line:78 but if content isn't read it never occurs
Console.WriteLine(ex.ToString());
return false;
}
}
}
}
}
感谢您给我的任何帮助/建议!
【问题讨论】:
-
服务器可能不允许同时来自同一客户端的两个连接,这会导致错误。不确定。或者您正在使用具有相同源和目标的相同端口号。也许在 80 和 8080 等并行连接上尝试不同的端口号。
-
嗨@jdweng,奇怪的是这个问题很少随机发生 - 我解释自己:当我运行测试应用程序时,如果发生异常,下一个调用仍然通过(它不会再次发生)。同时,呼叫和内容似乎已完成并在失败时收到......我尝试了不同的端口技巧但没有运气。
-
当两个连接发生的时间非常接近时,您认为会发生故障吗?这可能是一个已知的核心问题。如果您使用网络,也会发生同样的情况吗?请参阅:docs.microsoft.com/en-us/dotnet/core/compatibility/3.1
-
听起来@jdweng 是对的,你可以通过每次创建一个新的cookie容器来避免它(这很糟糕,因为你可能需要重新授权)
var cookieContainer = new CookieContainer(); using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer }) using (var client = new HttpClient(handler) { BaseAddress = baseAddress }) {} -
你应该建立你自己的服务器来测试这个代码,不要依赖一些在线测试服务器,因为这可能是由那个服务器(不是你的代码)引起的。这个错误看起来确实像这样。
标签: c# asp.net-core task-parallel-library asp.net-core-webapi dotnet-httpclient