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



我有一个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);

当我尝试在没有端点服务运行的情况下进行测试时,对于第一次重试尝试,重试处理程序被触发,并且我的日志记录器记录了第一次尝试。然而,在第二次重试尝试时,我得到一个错误消息说:

请求消息已经发送。不能发送相同的请求消息多次

我知道其他人也遇到了类似的问题(参见重试HttpClient不成功的请求,解决方案似乎是做我正在做的(即使用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<string> policyRegistry中注入的全部原因-我不能在启动时执行此操作)。另一个好处是单元测试——我可以简单地用相同的策略注入相同的集合,而不需要复制和粘贴一大堆代码,使单元测试变得多余,因为我不再测试我的服务了。在启动时定义策略使这种情况不可能发生。所以我的问题是,有没有一种方法可以避免使用这种方法得到重复的请求错误?

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

AddPolicyHandler添加的PolicyHttpMessageHandler将创建一个PollyContext,如果还没有附加。因此,您可以添加一个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>,创建一个包含该记录器的PollyContext,然后调用PolicyHttpMessageHandler,它使用现有的PollyContext,因此您的重试策略可以成功使用context.TryGetLogger

您太沉迷于Polly以及如何配置它,而忘记了一些基本方面。别担心,这太简单了!

首先,不能多次发送相同的HttpRequestMessage。请看这篇关于这个主题的广泛问答。它也有官方文档,尽管文档对原因有点不透明。

其次,当您对它进行编码时,您创建的请求被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,如果它们不更改请求,则可能是OK的,但您可能还需要将其他变量移动到lambda中。

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

相关内容

  • 没有找到相关文章

最新更新