提问人:Sergio Jr 提问时间:9/13/2023 最后编辑:Peter CsalaSergio Jr 更新时间:9/16/2023 访问量:170
在 HttpClient 超时处理 TaskCanceledException
Handling TaskCanceledException when HttpClient times out
问:
我正在尝试使用策略处理程序进行重试或 http 暂时性错误,但它无法正常工作。Microsoft.Extensions.Http.Polly
TaskCanceledException
我在下面编写了最小的 API,只是为了测试:
Program.cs
using Polly;
using Polly.Extensions.Http;
using Polly.Retry;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
AsyncRetryPolicy<HttpResponseMessage>? policy = HttpPolicyExtensions.HandleTransientHttpError()
.Or<TaskCanceledException>()
.WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(5));
builder.Services.AddHttpClient("test", client =>
{
client.BaseAddress = new Uri("http://localhost:5000");
client.Timeout = TimeSpan.FromSeconds(2);
})
.AddPolicyHandler(policy);
WebApplication app = builder.Build();
app.MapPost("/status/{code}",
async (string code, IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
{
HttpClient client = httpClientFactory.CreateClient("test");
HttpResponseMessage response = await client.GetAsync($"/status/{code}", cancellationToken);
response.EnsureSuccessStatusCode();
return response.Content.ReadAsStringAsync(cancellationToken);
});
app.MapPost("/timeout/{timeout:int}",
async (int timeout, IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
{
HttpClient client = httpClientFactory.CreateClient("test");
HttpResponseMessage response = await client.GetAsync($"/timeout/{timeout}", cancellationToken);
response.EnsureSuccessStatusCode();
return response.Content.ReadAsStringAsync(cancellationToken);
});
#region Simulated API
app.MapGet("/status/{code:int}", (int code) => Results.StatusCode(code));
app.MapGet("/timeout/{timeout:int}", async (int timeout) =>
{
await Task.Delay(TimeSpan.FromSeconds(timeout));
return Results.Ok();
});
#endregion
app.Run();
调用后,它会引发此异常,而无需任何进一步的尝试:POST http://localhost:5000/timeout/10
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 2 seconds elapsing.
---> System.TimeoutException: A task was canceled.
---> System.Threading.Tasks.TaskCanceledException: A task was canceled.
at Polly.Retry.AsyncRetryEngine.ImplementationAsync[TResult](Func`3 action, Context context, CancellationToken cancellationToken, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Func`5 onRetryAsync, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider, Boolean continueOnCapturedContext)
at Polly.AsyncPolicy`1.ExecuteAsync(Func`3 action, Context context, CancellationToken cancellationToken, Boolean continueOnCapturedContext)
at Microsoft.Extensions.Http.PolicyHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.<>c__DisplayClass5_0.<<SendCoreAsync>g__Core|0>d.MoveNext()
--- End of stack trace from previous location ---
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
--- End of inner exception stack trace ---
--- End of inner exception stack trace ---
at System.Net.Http.HttpClient.HandleFailure(Exception e, Boolean telemetryStarted, HttpResponseMessage response, CancellationTokenSource cts, CancellationToken cancellationToken, CancellationTokenSource pendingRequestsCts)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at Program.<>c.<<<Main>$>b__0_3>d.MoveNext() in Z:\Repositories\POCs\PollyRetryTest\PollyRetryTest\Program.cs:line 35
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<ExecuteTaskOfT>g__ExecuteAwaited|129_0[T](Task`1 task, HttpContext httpContext, JsonTypeInfo`1 jsonTypeInfo)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
我做错了什么?是否必须重写主处理程序?
答:
3赞
Peter Csala
9/13/2023
#1
TL;DR:HttpClient 的超时作为每个逻辑请求的超时(涵盖所有重试尝试)而不是每个传输请求(涵盖单个尝试)。
让我们看看我们这里有什么:
- 您运行时间很长(10 秒)
- 您有一个重试策略,其中最多重试 5 次,每次尝试之间有 5 秒的延迟
- HttpClient 上的每个逻辑请求有 2 秒的超时
- 因此,它不是每个传输请求的超时
让我们看看会发生什么:
- 针对长时间运行的操作发出请求
- HttpClient 的超时开始生效
- 重试策略启动
- 如果为 / 添加日志记录,您可以看到它只触发一次
onRetry
onRetryAsync
- 如果为 / 添加日志记录,您可以看到它只触发一次
- 然后它进入睡眠状态 5 秒钟
- HttpClient 的调用会引发 TCE,因为每个逻辑请求超时已启动
GetAsync
解决方案
使用超时策略定义每个传输请求的超时
IAsyncPolicy<HttpResponseMessage> timeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2));
IAsyncPolicy<HttpResponseMessage> retry = HttpPolicyExtensions.HandleTransientHttpError()
.Or<TaskCanceledException>()
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(5),
onRetry:(_, __) => Console.WriteLine("RETRIED"));
builder.Services.AddHttpClient("test", client =>
{
client.BaseAddress = new Uri("http://localhost:5000");
client.Timeout = TimeSpan.FromSeconds(20);
})
.AddPolicyHandler(Policy.WrapAsync(retry, timeout));
- 将用作每个传输请求的超时(2 秒)
timeout
- 超时策略感知 (
retry
Or<TimeoutRejectedException>()
)- 我添加了一些日志记录,以查看重试启动的次数
- 我已将 HttpClient 上的每个逻辑请求超时从 2 秒增加到 20 秒
- 我已将 和 策略结合起来进行协作
timeout
retry
您应该在日志中看到类似的内容:
RETRIED
...
RETRIED
...
RETRIED
...
TaskCancelledException thrown
让我们看一下操作的顺序:
- [00-02s]:初始请求抛出
TimeoutRejectedException
- @2s:印刷品
RETRIED
- [02-07s]:波莉在睡觉
- [07-09s]:第一次重试尝试抛出
TimeoutRejectedException
- @9s:印刷品
RETRIED
- [09-14s]:波莉睡着了
- [14-16s]:第 2 次重试尝试抛出
TimeoutRejectedException
- @16s: 印刷品
RETRIED
- [16-21s]:波莉在睡觉
- @20s:投掷
HttpClient
TaskCancelledException
注意
请注意,您可以使用 Polly 实现 per-logical-request 和 per-transport-request 超时
IAsyncPolicy<HttpResponseMessage> attemptSequenceTimeout = Policy
.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(20));
IAsyncPolicy<HttpResponseMessage> attemptTimeout = Policy
.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2));
IAsyncPolicy<HttpResponseMessage> retry = HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(5),
onRetry:(_, __) => Console.WriteLine("RETRIED"));
var combinedPolicy = Policy.WrapAsync(
attemptSequenceTimeout,
retry,
attemptTimeout);
builder.Services.AddHttpClient("test", client =>
{
client.BaseAddress = new Uri("http://localhost:5000");
})
.AddPolicyHandler(combinedPolicy);
评论
Polly.Extensions.Http
Microsoft.Extensions.Http.Polly