使用 Polly

问题描述

我有一个 Azure 函数,可以对 webapi 端点进行 http 调用。我正在关注此示例 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<string> 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)。但是,我不明白如果我将重试策略定义为 Startup 中的配置的一部分,则问题如下:

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<string> policyRegistry 中注入的全部原因 - 我不能在启动时这样做)。另一个好处是单元测试 - 我可以简单地将相同的策略注入到同一个集合中,而无需复制和粘贴一大堆代码并使单元测试变得多余,因为我不再测试我的服务。在启动中定义策略使这成为不可能。所以我的问题是,有没有办法使用这种方法来避免重复请求错误

解决方法

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

PolicyHttpMessageHandler 添加的 AddPolicyHandler 将创建一个 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<MyService> 获取一个 ILogger<MyService>,创建一个包含该记录器的 Polly Context,然后调用 PolicyHttpMessageHandler,它使用现有的 Polly Context ,因此您的重试策略可以成功使用 context.TryGetLogger

,

您对 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);
}

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

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