【问题标题】:Polly retry not always catching HttpRequestExceptionPolly 重试并不总是捕获 HttpRequestException
【发布时间】:2022-03-03 21:18:22
【问题描述】:

我的 .NET Core 3.1 应用使用 Polly 7.1.0 重试和隔板策略来实现 http 弹性。重试策略使用HandleTransientHttpError() 捕获可能的HttpRequestException

现在使用MyClient 触发的http 请求有时会返回HttpRequestException。其中大约一半被波莉抓住并重试。然而,另一半最终出现在我的 try-catch-block 中,我必须手动重试它们。这发生在最大重试次数用尽之前。

我是如何设法创建一个竞争条件来阻止 Polly 捕获所有异常的?我该如何解决这个问题?

我使用IHttpClientFactory 注册策略如下。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient<MyClient>(c =>
    {
        c.BaseAddress = new Uri("https://my.base.url.com/");
        c.Timeout = TimeSpan.FromHours(5); // Generous timeout to accomodate for retries
    })
        .AddPolicyHandler(GetHttpResiliencePolicy());
}

private static AsyncPolicyWrap<HttpResponseMessage> GetHttpResiliencePolicy()
{
    var delay = Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromSeconds(1), retryCount: 5);

    var retryPolicy = HttpPolicyExtensions
            .HandleTransientHttpError() // This should catch HttpRequestException
            .OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound)
            .WaitAndRetryAsync(
                sleepDurations: delay,
                onRetry: (response, delay, retryCount, context) => LogRetry(response, retryCount, context));

    var throttlePolicy = Policy.BulkheadAsync<HttpResponseMessage>(maxParallelization: 50, maxQueuingActions: int.MaxValue);

    return Policy.WrapAsync(retryPolicy, throttlePolicy);
}

触发 http 请求的MyClient 如下所示。

public async Task<TOut> PostAsync<TOut>(Uri requestUri, string jsonString)
{
    try
    {
        using (var content = new StringContent(jsonString, Encoding.UTF8, "application/json"))
        using (var response = await httpClient.PostAsync(requestUri, content)) // This throws HttpRequestException
        {
            // Handle response
        }
    }
    catch (HttpRequestException ex)
    {
        // This should never be hit, but unfortunately is
    }
}

这里有一些附加信息,虽然我不确定它是否相关。

  1. 由于 HttpClientDI-registered transiently,因此每个工作单元有 10 个实例飞来飞去。
  2. 每个工作单元,客户端会触发约 400 个 http 请求。
  3. http 请求很长(5 分钟持续时间,30 MB 响应)

【问题讨论】:

  • 我在您的 Polly 设置中没有发现任何明显的错误。您能否创建一个最小的复制品,最好使用本地服务?我会说,问题可能不是 Polly,它是我见过的经过最充分测试的库之一。
  • @MatthiasSchuchart 为什么你有NotFound 的触发器?您是否尝试处理最终的一致性?
  • @PeterCsala 我不经常使用的网络服务在背压下返回 404。相同的请求通常在重试或 Web 服务空闲时返回 200。由于我无法区分“真实”和基于负载的 404,不幸的是,我还需要重试“真实”404 5 次。
  • @PeterCsala 同意,网络服务应该使用 429,不幸的是,这超出了我的控制。并且还同意我可以使用断路器给它一些呼吸空间(尽管DecorrelatedJitterBackoffV2 中的指数退避应该达到类似的效果)。然而,问题的关键在于确定,什么可能使 Polly 错过它应该捕获的异常。评估这一点也可能使其他人受益。
  • @MatthiasSchuchardt 我已经留下了答案,请检查一下。关于 CB 与退避:它们不一样。在回退的情况下,允许向目标服务器发出所有即将到来的请求。在 CB 的情况下,所有即将到来的请求都被阻塞,直到 CB 处于 HalfOpen 状态时请求成功。

标签: c# http asp.net-core polly


【解决方案1】:

重试和HttpRequestException

当我们谈论 Polly 政策时,我们可以区分两种不同的例外情况:

  • 已处理
  • 未处理。

处理异常

  • 它触发给定策略的某种行为(在本例中为HttpRequestException)。
  • 如果策略无法成功,则再次抛出已处理的异常。
  • 如果有其他策略,那么它可能会也可能不会处理该异常。

未处理的异常

  • 它不会引起任何类型的反应(例如在我们的例子中是WebException)。
  • 未处理的异常流经策略。
  • 如果有其他策略,那么它可能会也可能不会处理该异常。

“其中大约一半被 Polly 捕获并重试。
然而,另一半最终出现在我的 try-catch-block 中”

如果您的某些重试次数用完,就会发生这种情况。换句话说,有一些请求在 6 次尝试(5 次重试和 1 次初始尝试)中无法成功。

这可以通过以下两种工具之一轻松验证:

  • onRetry + context
  • Fallback + context

onRetry + context

onRetry 在触发重试策略时但在睡眠持续时间之前被调用。代表收到retryCount。因此,为了能够连接/关联同一请求的单独日志条目,您需要使用某种相关 ID。最简单的方法是这样编码:

public static class ContextExtensions
{
    private const string Key = "CorrelationId";

    public static Context SetCorrelation(this Context context, Guid? id = null)
    {
        context[Key] = id ?? Guid.NewGuid();
        return context;
    }

    public static Guid? GetCorrelation(this Context context)
    {
        if (!context.TryGetValue(Key, out var id))
            return null;

        if (id is Guid correlation)
            return correlation;

        return null;
    }
}

这是一个简化的例子:
要执行的方法

private async Task<string> Test() 
{ 
    await Task.Delay(1000); 
    throw new CustomException(""); 
}

政策

var retryPolicy = Policy<string>
    .Handle<CustomException>()
    .WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(1),
        (result, delay, retryCount, context) =>
        {
            var id = context.GetCorrelation();
            Console.WriteLine($"{id} - #{retryCount} retry.");
        });

用法

var context = new Context().SetCorrelation();
try
{
    await retryPolicy.ExecuteAsync(async (ctx) => await Test(), context);
}
catch (CustomException)
{
    Console.WriteLine($"{context.GetCorrelation()} - All retry has been failed.");
}

样本输出

3319cf18-5e31-40e0-8faf-1fba0517f80d - #1 retry.
3319cf18-5e31-40e0-8faf-1fba0517f80d - #2 retry.
3319cf18-5e31-40e0-8faf-1fba0517f80d - #3 retry.
3319cf18-5e31-40e0-8faf-1fba0517f80d - #4 retry.
3319cf18-5e31-40e0-8faf-1fba0517f80d - #5 retry.
3319cf18-5e31-40e0-8faf-1fba0517f80d - All retry has been failed.

Fallback

正如人们所说,只要策略不能成功,它就会重新抛出已处理的异常。换句话说,如果一个策略失败了,那么它会将问题升级到下一个级别(下一个外部策略)。

这是一个简化的例子:
政策

var fallbackPolicy = Policy<string>
    .Handle<CustomException>()
    .FallbackAsync(async (result, ctx, ct) =>
    {
        await Task.FromException<CustomException>(result.Exception);
        return result.Result; //it will never be executed << just to compile
    }, 
    (result, ctx) =>
    {
        Console.WriteLine($"{ctx.GetCorrelation()} - All retry has been failed.");
        return Task.CompletedTask;
    });

用法

var context = new Context().SetCorrelation();
try
{
    var strategy = Policy.WrapAsync(fallbackPolicy, retryPolicy);  
    await strategy.ExecuteAsync(async (ctx) => await Test(), context);
}
catch (CustomException)
{
    Console.WriteLine($"{context.GetCorrelation()} - All policies failed.");
}

样本输出

169a270e-acf7-45fd-8036-9bd1c034c5d6 - #1 retry.
169a270e-acf7-45fd-8036-9bd1c034c5d6 - #2 retry.
169a270e-acf7-45fd-8036-9bd1c034c5d6 - #3 retry.
169a270e-acf7-45fd-8036-9bd1c034c5d6 - #4 retry.
169a270e-acf7-45fd-8036-9bd1c034c5d6 - #5 retry.
169a270e-acf7-45fd-8036-9bd1c034c5d6 - All retry has been failed.
169a270e-acf7-45fd-8036-9bd1c034c5d6 - All policies failed.

【讨论】:

  • 感谢您的详细回答。我已经有了这个机制。这就是我在catch-block 中调用的LogRetry()-方法中发生的事情。我使用请求正文作为唯一标识符。这就是我首先确定未捕获的异常发生在重试用尽之前 的方式。在一些日志文件中,我可以看到在记录一次 Polly 重试之前未捕获异常。
  • @MatthiasSchuchart 嗯,这很奇怪。那么我唯一能想到的就是你的HttpClient周围的using。没有试过吗?也可以请检查什么是innerException,以防顶级捕获HttpRequestException.
  • 内部异常是IOException: The response ended prematurely.,没有进一步的嵌套级别。删除 usings 没有任何效果。我仍然偶尔会遇到catch-block。
  • 这很奇怪。 The response ended prematurely 不应该是最后一个例外。通常它有一个innerException。您是记录所有异常层次结构还是只记录顶级异常及其内部?
  • 我记录了完整的异常层次结构。另外,当我调试应用程序并在catch-block 中放置断点时,我可以看到IOExceptionInnerException-property 是null
猜你喜欢
  • 2021-12-13
  • 1970-01-01
  • 2012-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多