Политика HttpClient Polly WaitAndRetry - PullRequest
3 голосов
/ 09 июля 2020

Кто-нибудь знает, почему приведенная ниже политика прекращает повторные попытки после 3 раз, а не 10?

IAsyncPolicy<HttpResponseMessage> httpWaitAndRetryPolicy =
     Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
          .OrHandle<Exception>(r => true)
          .WaitAndRetryAsync(10, retryAttempt => TimeSpan.FromSeconds(2));

Я установил для попытки повтора значение 10 и тестирую HTTP-вызов с ошибкой BadRequest. Но он повторил попытку только 3 раза, а затем остановился до истечения времени ожидания и выбросил исключение

  ----> System.Threading.Tasks.TaskCanceledException : A task was canceled.
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at HttpRetry.Lab.Tests.ServiceTest.WhenPostWrongAlert_ThenRecoversProperly() in C:\ServiceTest.cs:line 56
--TaskCanceledException


15:57:03.6367  INFO HttpClientProvider - Configuring client xxxxxxxx:1234/api/" timeout=00:02:00
15:57:03.6636  INFO Service            - POST xxxx/xxxxxxx
15:57:04.2051  INFO HttpClientProvider - Retrying retryCount=1 sleepDuration=00:00:02 result=Polly.DelegateResult`1[System.Net.Http.HttpResponseMessage]
15:57:06.6880  INFO HttpClientProvider - Retrying retryCount=2 sleepDuration=00:00:02 result=Polly.DelegateResult`1[System.Net.Http.HttpResponseMessage]
15:59:03.6811  INFO HttpClientProvider - Retrying retryCount=3 sleepDuration=00:00:02 result=Polly.DelegateResult`1[System.Net.Http.HttpResponseMessage]
15:59:03.6811 ERROR ServiceTest - System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at HttpRetry.Lab.Service.<PostAsync>d__4.MoveNext() in C:\Service.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at HttpRetry.Lab.Tests.ServiceTest.<PostAsync>d__4.MoveNext() in C:\ServiceTest.cs:line 27

var serviceProvider = serviceConnection.AddHttpClient(connection.Name, c =>
        {
            c.BaseAddress = new Uri(connection.BaseUrl);
            c.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{connection.UserName}:{connection.Password}")));
            c.Timeout = connection.Timeout; // Timeout is TimeSpan.FromSeconds(120)
        })
    .AddPolicyHandler(httpWaitAndRetryPolicy)
    .Services.BuildServiceProvider();

HttpClientFactories.Add(connection.Name, serviceProvider.GetService<IHttpClientFactory>());

Подтверждена root причина проблемы: я не знаю, что вызывает симптом, но похоже на запрос соединение не будет разорвано, если явно не вызвать Dispose HttpResponseMessage OnRetry. Текущее решение - настроить OnRetry в WaitAndRetryAsyn c и удалить ответ. Все работает нормально без необходимости изменения ServicePointManager.DefaultConnectionLimit

Ответы [ 2 ]

2 голосов
/ 10 июля 2020

Ваш тайм-аут

Как я могу видеть на основе вашего кода, у вас есть глобальный тайм-аут в 1 минуту на уровне HttpClient. Это вызовет TaskCanceledException, хотя вы могли бы ожидать TimeoutException.

Если вы wi sh для получения TimeoutException, тогда вам нужно указать тайм-аут на основе запроса / уровня через свойство RequestTimeout из HttpRequestMessage. Для получения дополнительной информации проверьте следующую ссылку .

Ваш повтор

Ваш лог повторных попыток c определяет 3 (или 10) повторных попыток со штрафом в 5 секунд. 3 попытки означают 4 попытки, потому что есть начальный (0-й) запрос, который находится за пределами области повтора. Если это не удается, то первая попытка станет второй попыткой.

Таким образом, поток будет выглядеть так:

  1. Первоначальный запрос отправлен << 1-я попытка </li>
  2. Не удалось выполнить первоначальный запрос
  3. Retry logi c запущен
  4. выставлен штраф в 5 секунд
  5. 1st retry logi c срабатывает << 2nd попытка </li>
  6. Вторая попытка не удалась
  7. Повторная попытка c срабатывает
  8. выставлен штраф в 5 секунд
  9. Вторая попытка logi c срабатывает << 3-я попытка </li>
  10. ...

Если все это можно завершить менее чем за секунду, то HttpClient выдаст TaskcCanceledExpcetion из-за глобального тайм-аута.

Политика тайм-аута Polly

Polly также поддерживает локальные и глобальные политики тайм-аута. Существует единственная политика тайм-аута , которую можно использовать обоими способами.

Она может действовать как локальный тайм-аут, если ваша политика тайм-аута заключена в повторную попытку: retryPolicy.WrapAsync(timeoutPolicy);

Он может действовать как глобальный тайм-аут, если ваша политика повтора заключена в тайм-аут: retryPolicy.WrapAsync(timeoutPolicy);

Конечно, у вас могут быть глобальные и локальные тайм-ауты одновременно: Policy.WrapAsync(globalTimeoutPolicy, retryPolicy, localTimeoutPolicy);

Я настоятельно рекомендую вам рассмотреть возможность использования тайм-аута Polly вместо тайм-аута HttpClient, чтобы иметь единое место, где вы определяете свою устойчивую стратегию.

Пожалуйста, имейте в виду, что политика тайм-аута будет вызывать TimeoutRejectedException, если время ожидания истекло без ответа. Поскольку ваша повторная попытка обрабатывает всевозможные исключения (.OrHandle<Exception>()), вам не нужно изменять политику повторных попыток.

Обработка ошибок временного сбоя Polly

Существует пакет nuget с именем Microsoft.Extensions.Http.Polly ( 1 ), определяющий несколько полезных утилит. Один из них - HttpPolicyExtensions.HandleTransientHttpError()

. Он ловит HttpRequestException и проверяет, является ли код состояния ответа 5xx или 408 (RequestTimeout).

Возможно, стоит подумать об использовании и этого.

Политики отладки

Каждая из разных политик определяет обратный вызов, чтобы дать возможность понять, как они работают. В случае повторной попытки он называется onRetry или onRetryAsync для syn c или asyn c retry соответственно. Вы, указав в своем WaitAndRetryAsync следующего делегата, можете получить действительно полезную информацию:

onRetryAsync: (exception, delay, times, context) => {
  //TODO: logging
}
0 голосов
/ 15 июля 2020

В результате наша команда обнаружила, что политика Polly Retry не освобождает соединение HTTP-запроса до тех пор, пока не будет использован HttpResponseMessage. Если быть точным, это не имеет ничего общего с Polly Retry, просто соединение не будет разорвано до тех пор, пока исходный HttpClient.SendAsyn c не вернется. И политика повтора - это своего рода задержка, которая происходит из-за WaitAndRetry. Закончить политику повторных попыток Polly (например, x раз) может закончиться использованием x + 1 одновременных http-соединений для каждого BadRequest.

Есть 2 способа «использовать» HttpResponseMessage. Либо явно вызовите response.Result.Dispose, либо выполнив какое-то чтение для содержимого ответа. например response.Result.ReadAsAsyn c. Ну, еще один способ - дождаться тайм-аута httpClient, но я считаю, что это не совсем то, что нам нужно. Вот код, который заставляет все работать. Ключ - HttpResponse.Dispose в OnRetry

    ServicePointManager.DefaultConnectionLimit = appConfiguration.HttpClients.ConnectionLimit;

    IAsyncPolicy<HttpResponseMessage> httpWaitAndRetryPolicy =
        Policy.Handle<HttpRequestException>()
              .Or<Exception>()
              .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
              .WaitAndRetryAsync(3, 
                                 retryAttempt => TimeSpan.FromSeconds(appConfiguration.HttpClients.RetryFactor * retryAttempt), 
                                 OnRetry);

    IAsyncPolicy<HttpResponseMessage> timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(appConfiguration.HttpClients.RequestTimeout);

    foreach (var endpoint in appConfiguration.HttpClients.HttpEndpoints)
    {
        var serviceProvider = serviceConnection.AddHttpClient(endpoint.Name,
                              c =>
                              {
                                  c.BaseAddress = new Uri(endpoint.BaseUrl);
                                  c.DefaultRequestHeaders.Authorization   = new System.Net.Http.Headers.AuthenticationHeaderValue(endpoint.AuthenticationScheme, Convert.ToBase64String(Encoding.ASCII.GetBytes($"{endpoint.UserName}:{endpoint.Password}")));
                                  c.DefaultRequestHeaders.ConnectionClose = false;
                                  c.Timeout     = endpoint.Timeout;
                              }).AddPolicyHandler(Policy.WrapAsync(httpWaitAndRetryPolicy, timeoutPolicy))
                              .Services.BuildServiceProvider();

        httpClientFactories.Add(endpoint.Name, serviceProvider.GetService<IHttpClientFactory>());
    }

    private Task OnRetry(DelegateResult<HttpResponseMessage> response, TimeSpan span, int retryCount, Context context)
    {
        if (response == null)
            return Task.CompletedTask;

        var responseResult = response.Result;
        logger.Info($"RetryCount={retryCount} IsSuccess={responseResult == null ? "" : responseResult.IsSuccessStatusCode} StatusCode={responseResult == null ? "" : responseResult.StatusCode} Exception={response.Exception?.Message});

        response.Result?.Dispose();
        return Task.CompletedTask;
    }
...