簡體   English   中英

使用 Polly 重試 HttpClient 請求的正確方法

[英]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> 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 )。但是,如果我將重試策略定義為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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM