[英]Correct way to retry HttpClient requests with Polly
我有一個 Azure function 對 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> 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);
當我嘗試在沒有端點服務運行的情況下對此進行測試時,對於第一次重試嘗試,重試處理程序被觸發並且我的記錄器記錄了第一次嘗試。 但是,在第二次重試時,我收到一條錯誤消息:
請求消息已發送。 不能多次發送同一個請求報文
我知道其他人也遇到過類似的問題(請參閱重試 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));
}
並簡單地調用我的服務:
var response = await httpClient.SendAsync(requestMessage);
但是這樣做我失去了在重試策略上下文中傳遞我的記錄器的能力(這是我在IReadOnlyPolicyRegistry<string> policyRegistry
中注入的全部原因 - 我不能在啟動時這樣做)。 另一個好處是用於單元測試——我可以簡單地使用相同的策略注入相同的集合,而無需復制和粘貼一大堆代碼並使單元測試變得多余,因為我不再測試我的服務。 在啟動時定義策略使這成為不可能。 所以我的問題是,有沒有一種方法可以避免使用這種方法出現重復請求錯誤?
這是一個替代解決方案(我更喜歡)。
PolicyHttpMessageHandler
添加的AddPolicyHandler
將創建一個 Polly Context
(如果尚未附加)。 因此,您可以添加一個創建Context
並附加記錄器的MessageHandler
:
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
。 請參閱有關該主題的廣泛問答。 它也被正式記錄,盡管文檔在原因上有點不透明。
其次,當您對其進行編碼時,您創建的請求被 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 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(() =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return httpClient.SendAsync(requestMessage), context);
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.