使用 Polly 重试 HttpClient 请求的正确方法

aub*_*urg 3 c# .net-core polly azure-functions

我有一个 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"));
});
Run Code Online (Sandbox Code Playgroud)

我的重试策略如下所示:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我的客户端发送服务在其构造函数中接收IReadOnlyPolicyRegistry<string> policyRegistry和。IHttpClientFactory 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);
Run Code Online (Sandbox Code Playgroud)

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

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

我知道其他人也遇到过类似的问题(请参阅重试 HttpClient 失败的请求,解决方案似乎是做我正在做的事情(即使用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));
}
Run Code Online (Sandbox Code Playgroud)

并简单地调用我的服务:

var response = await httpClient.SendAsync(requestMessage);
Run Code Online (Sandbox Code Playgroud)

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

Ste*_*ary 7

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

如果尚未附加 Polly,则添加PolicyHttpMessageHandlerAddPolicyHandler将创建一个 Polly 。因此,您可以添加一个来创建并附加记录器:Context MessageHandlerContext

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

注册的一个小扩展方法使它变得很好:

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>>();
}
Run Code Online (Sandbox Code Playgroud)

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

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

在运行时,LoggerProviderMessageHandler<MyService>获取ILogger<MyService>,创建一个Context包含该记录器的 Polly,然后调用PolicyHttpMessageHandler,它使用现有的 Polly Context,以便您的重试策略可以成功使用context.TryGetLogger