【问题标题】:Correct way to retry HttpClient requests with Polly使用 Polly 重试 HttpClient 请求的正确方法
【发布时间】:2021-06-07 17:31:19
【问题描述】:

我有一个对 webapi 端点进行 http 调用的 Azure 函数。我正在关注此示例GitHub Polly RetryPolicy,因此我的代码具有类似的结构。所以在 Startup.cs 我有:

        builder.Services.AddPollyPolicies(config); // extension methods setting up Polly retry policies
        builder.Services.AddHttpClient("MySender", client =>
        {
            client.BaseAddress = config.SenderUrl;
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        });

我的重试策略如下所示:

public static class PollyRegistryExtensions
    {
        public static IPolicyRegistry<string> AddBasicRetryPolicy(this IPolicyRegistry<string> policyRegistry, IMyConfig config)
        {
            var retryPolicy = Policy
                .Handle<Exception>()
                .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
                .WaitAndRetryAsync(config.ServiceRetryAttempts, retryCount => TimeSpan.FromMilliseconds(config.ServiceRetryBackOffMilliSeconds), (result, timeSpan, retryCount, context) =>
                {
                    if (!context.TryGetLogger(out var logger)) return;

                    logger.LogWarning(
                        $"Service delivery attempt {retryCount} failed, next attempt in {timeSpan.TotalMilliseconds} ms.");

                })
                .WithPolicyKey(PolicyNames.BasicRetry);

            policyRegistry.Add(PolicyNames.BasicRetry, retryPolicy);

            return policyRegistry;
        }
    }

我的客户端发件人服务在其构造函数中接收IReadOnlyPolicyRegistry&lt;string&gt; policyRegistryIHttpClientFactory clientFactory。我调用客户端的代码如下:

           var jsonContent =  new StringContent(JsonSerializer.Serialize(contentObj),
                Encoding.UTF8,
                "application/json");

            HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
            {
                Content = jsonContent
            };

            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
            requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>(PolicyNames.BasicRetry)
                              ?? Policy.NoOpAsync<HttpResponseMessage>();

            var context = new Context($"GetSomeData-{Guid.NewGuid()}", new Dictionary<string, object>
            {
                { PolicyContextItems.Logger, _logger }
            });

            var httpClient = _clientFactory.CreateClient("MySender");

            var response = await retryPolicy.ExecuteAsync(ctx =>
                httpClient.SendAsync(requestMessage), context);

当我尝试在没有运行端点服务的情况下进行测试时,第一次重试尝试时,重试处理程序被触发并且我的记录器记录了第一次尝试。但是,在第二次重试时,我收到一条错误消息:

请求消息已发送。无法发送相同的请求 多次留言

我知道其他人也遇到过类似的问题(请参阅Retrying HttpClient Unsuccessful Requests,解决方案似乎是做我正在做的事情(即使用HttpClientFactory)。但是,如果我定义,我不会遇到这个问题我的重试策略作为启动中配置的一部分如下:

builder.Services.AddHttpClient("MyService", client =>
            {
                client.BaseAddress = config.SenderUrl;
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            }).AddPolicyHandler(GetRetryPolicy());

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
        {
            return HttpPolicyExtensions
                .HandleTransientHttpError()
                .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
                .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromMilliseconds(1000));
        }

然后简单地调用我的服务:

var response = await httpClient.SendAsync(requestMessage);

但是这样做我失去了在重试策略上下文中传递我的记录器的能力(这是我在IReadOnlyPolicyRegistry&lt;string&gt; policyRegistry 中注入的全部原因 - 我不能在启动时这样做)。另一个好处是单元测试 - 我可以简单地使用相同的策略注入同一个集合,而无需复制和粘贴一大堆代码并使单元测试变得多余,因为我不再测试我的服务。在启动中定义策略使这成为不可能。所以我的问题是,有没有办法不使用这种方法得到这个重复的请求错误?

【问题讨论】:

    标签: c# .net-core azure-functions polly


    【解决方案1】:

    这是一个替代解决方案(我更喜欢)。

    AddPolicyHandler 添加的PolicyHttpMessageHandler 将创建一个 Polly Context 如果尚未附加。所以你可以添加一个MessageHandler 来创建一个Context 并附加记录器:

    public sealed class LoggerProviderMessageHandler<T> : DelegatingHandler
    {
        private readonly ILogger _logger;
    
        public LoggerProviderMessageHandler(ILogger<T> logger) => _logger = logger;
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var httpClientRequestId = $"GetSomeData-{Guid.NewGuid()}";
            var context = new Context(httpClientRequestId);
            context[PolicyContextItems.Logger] = _logger;
            request.SetPolicyExecutionContext(context);
    
            return await base.SendAsync(request, cancellationToken);
        }
    }
    

    注册的一个小扩展方法很不错:

    public static IHttpClientBuilder AddLoggerProvider<T>(this IHttpClientBuilder builder)
    {
        if (!services.Any(x => x.ServiceType == typeof(LoggerProviderMessageHandler<T>)))
            services.AddTransient<LoggerProviderMessageHandler<T>>();
        return builder.AddHttpMessageHandler<LoggerProviderMessageHandler<T>>();
    }
    

    然后你可以这样使用它(注意它必须在AddPolicyHandler 之前,以便它首先创建Context):

    builder.Services.AddHttpClient("MyService", client =>
    {
        client.BaseAddress = config.SenderUrl;
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    })
        .AddLoggerProvider<MyService>()
        .AddPolicyHandler(GetRetryPolicy());
    

    在运行时,LoggerProviderMessageHandler&lt;MyService&gt; 获取一个 ILogger&lt;MyService&gt;,创建一个包含该记录器的 Polly Context,然后调用 PolicyHttpMessageHandler,它使用现有的 Polly Context,因此您的重试策略可以成功使用 @987654336 @。

    【讨论】:

    • 让你的方法奏效——非常优雅!唯一的缺点是它不像传入 IReadOnlyPolicyRegistry policyRegistry 那样对单元测试友好,但它确实使服务实现更加简洁。
    【解决方案2】:

    您对 Polly 以及如何配置它有太多了解,而忘记了一些基本方面。别担心,这太容易了!

    首先,您不能多次发送相同的HttpRequestMessage。有关该主题,请参阅此广泛的 Q&A。它也被记录在officially 中,尽管文档在原因上有点不透明。

    其次,当您对其进行编码时,您创建的请求被 lambda 捕获一次,然后一遍又一遍地重复使用。

    对于您的特殊情况,我会将在您传递给ExecuteAsync 的lambda 中创建请求。这每次都会给你一个新的请求。

    修改你的代码,

    var jsonContent =  new StringContent(
        JsonSerializer.Serialize(contentObj),
        Encoding.UTF8,
        "application/json");
    
    var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>PolicyNames.BasicRetry)
        ?? Policy.NoOpAsync<HttpResponseMessage>();
    
    var context = new Context(
        $"GetSomeData-{Guid.NewGuid()}",
        new Dictionary<string, object>
        {
            { PolicyContextItems.Logger, _logger }
        });
    
    var httpClient = _clientFactory.CreateClient("MySender");
    
    var response = await retryPolicy.ExecuteAsync(ctx =>
    {
        var requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
        {
            Content = jsonContent
        };
    
        requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
        requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        httpClient.SendAsync(requestMessage), context);
    }
    
    

    其他捕获:记录器、authToken,如果它们不更改请求而不是请求,则可能没问题,但您可能还需要在 lambda 中移动其他变量。

    完全不使用 Polly 使得大部分思考过程变得不必要,但使用 Polly,您必须记住重试和策略是跨时间和上下文发生的。

    【讨论】:

    • 谢谢!我不得不稍微调整你的答案: var response = await retryPolicy.ExecuteAsync(ctx => DoRequest(jsonContent, authToken), context);其中 DoRequest 包含大部分原始代码。
    猜你喜欢
    • 2022-10-02
    • 1970-01-01
    • 2020-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-14
    • 1970-01-01
    相关资源
    最近更新 更多