代码之家  ›  专栏  ›  技术社区  ›  Matthias Schuchardt

Polly retry不总是捕获HttpRequestException

  •  0
  • Matthias Schuchardt  · 技术社区  · 5 年前

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

    现在,http请求以 MyClient 有时返回 HttpRequestException .其中大约一半被波利抓获并重试。然而,另一半最终在我的生活中结束 try-catch -阻止,我必须手动重试。这种情况会发生 之前 已用尽最大重试次数。

    我是如何设法创造一种竞赛条件来阻止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请求的步骤如下所示。

    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. 自从 HttpClient 是 DI-registered transiently ,每个工作单元有10个实例。
    2. 每一个工作单元,客户机发出约400个http请求。
    3. http请求很长(持续5分钟,30 MB响应)
    0 回复  |  直到 5 年前
        1
  •  1
  •   Peter Csala Matheus Robert Lichtnow    5 年前

    重试,然后 HttpRequestException

    每当我们谈论波利政策时,我们都可以区分两个不同的例外:

    • 处理
    • 未经处理。

    处理异常

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

    未处理的异常

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

    “其中大约一半被波利抓获并重试。
    然而,另一半最终进入了我的试捕区”

    如果您的某些重试尝试失败,则可能会发生这种情况。换句话说,有些请求在6次尝试(5次重试和1次初始尝试)中都未能成功。

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

    • onRetry + context
    • Fallback + 上下文

    重试 + 上下文

    这个 重试 在触发重试策略但在睡眠持续时间之前调用。代表收到 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.
    

    退路

    正如前面所说,每当策略无法成功时,它就会重新抛出已处理的异常。换句话说,如果策略失败,那么它会将问题升级到下一个级别(下一个外部策略)。

    下面是一个简化的例子:
    政策

    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.