HttpClient超时时处理TaskCanceledException

问题描述 投票:0回答:1

我尝试使用

Microsoft.Extensions.Http.Polly
策略处理程序在
TaskCanceledException
或 http 瞬时错误时重试,但它无法正常工作。

我编写了下面的最小 API 只是为了测试:

程序.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)     

我做错了什么?我必须覆盖主处理程序吗?

c# asp.net dotnet-httpclient polly retry-logic
1个回答
0
投票

TL;DR:HttpClient 的超时用作全局超时(涵盖所有重试尝试。

让我们看看这里有什么:

  • 您长时间运行操作(10秒)
  • 您的重试策略最多可进行 5 次重试,每次尝试之间有 5 秒的延迟
  • HttpClient 上有 2 秒的“全局”超时
    • 所以,这不是每个请求超时

让我们看看会发生什么:

  • 您针对长时间运行的操作发出请求
  • HttpClient 超时
  • 重试政策生效
    • 如果您为
      onRetry
      /
      onRetryAsync
      添加日志记录,您可以看到它仅触发一次
  • 然后进入睡眠状态5秒
  • 自全局超时开始以来,
  • HttpClient 的
    GetAsync
    调用会抛出 TCE

解决方案

使用超时策略来定义每个请求的超时时间

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));
  • timeout
    将根据请求超时(2秒)使用
  • 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]:第二次重试尝试抛出
    TimeoutRejectedException
  • @16s:打印
    RETRIED
  • [16-21s]:波莉正在睡觉
  • @20s:
    HttpClient
    抛出
    TaskCancelledException

注意

请注意,您也可以使用超时策略来实现全局超时

IAsyncPolicy<HttpResponseMessage> globalTimeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(20));

IAsyncPolicy<HttpResponseMessage> localTimeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2));

IAsyncPolicy<HttpResponseMessage> retry = HttpPolicyExtensions.HandleTransientHttpError()
    .Or<TimeoutRejectedException>()
    .WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(5), 
        onRetry:(_, __) => Console.WriteLine("RETRIED"));

builder.Services.AddHttpClient("test", client =>
    {
        client.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddPolicyHandler(Policy.WrapAsync(globalTimeout, retry, localTimeout));
© www.soinside.com 2019 - 2024. All rights reserved.