Проверьте содержимое строки ответа перед повторной попыткой с Полли

Я работаю с очень нестабильным API. Иногда я получаю 500 Server Error с Timeout, в другой раз я тоже получаю 500 Server Error потому что я дал ему понять, что он не может справиться SqlDateTime overflow. Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM.,

Оба этих случая дают мне HttpRequestException но я могу посмотреть ответное сообщение с сервера и определить причину исключения. Если это ошибка тайм-аута, я должен повторить попытку. Если это неверный ввод, я должен повторно выдать исключение, потому что никакое количество повторных попыток не решит проблему неверных данных.

Что я хотел бы сделать с Полли, так это проверить ответное сообщение, прежде чем пытаться повторить попытку. Но все образцы, которые я видел до сих пор, включали только тип исключения.

Я придумал это до сих пор:

        HttpResponseMessage response = null;
        String stringContent = null;
        Policy.Handle<FlakyApiException>()
             .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
              async (exception, timeSpan, context) =>
            {
                response = await client.PostAsync(requestUri, new StringContent(serialisedParameters, Encoding.UTF8, "application/json"));
                stringContent = await response.Content.ReadAsStringAsync();

                if (response.StatusCode == HttpStatusCode.InternalServerError && stringContent.Contains("Timeout"))
                {
                    throw new FlakyApiException(stringContent);
                }
            });

Есть ли лучший способ сделать такую ​​проверку?

4 ответа

Решение

В общем, вы можете настроить политики Polly так, чтобы они реагировали на результаты выполнения (а не только на исключение), например, проверьте HttpResponseMessage.StatusCode с предикатом. Примеры здесь в readme Полли.

Однако не существует встроенного способа настройки единой политики Polly для дополнительного ответа на содержимое ответного сообщения. Это связано с тем, что (как показывает ваш пример) получение этого контента требует второго асинхронного вызова, который сам может вызвать сетевые ошибки.

Это приводит к сложностям в отношении того, как выразить (в простом синтаксисе) единую политику, которая управляет двумя разными асинхронными шагами с потенциально различной обработкой ошибок для каждого шага. Предыдущее обсуждение Полли Гитхуб: комментарии приветствуются.

Таким образом, когда последовательности требуется два отдельных асинхронных вызова, команда Polly в настоящее время рекомендует выражать это как две отдельные политики, аналогично примеру в конце этого ответа.


Конкретный пример в вашем вопросе может не сработать, потому что onRetryAsync делегат FlakyApiException) сама не охраняется политикой. Политика только защищает выполнение делегатов, выполненных через .Execute/ExecuteAsync(...),


Одним из подходов может быть использование двух политик: политики повторных попыток, которая повторяет все типичные исключения http и коды состояния, включая 500; затем внутри этого Polly FallbackPolicy, который перехватывает код состояния 500, представляющий SqlDateTime overflowи исключает повторную попытку повторного броска в качестве некоторого отличительного исключения (CustomSqlDateOverflowException).

        IAsyncPolicy<HttpResponseMessage> rejectSqlError = Policy<HttpResponseMessage>
            .HandleResult(r => r.StatusCode == HttpStatusCode.InternalServerError)
            .FallbackAsync(async (delegateOutcome, context, token) =>
            {
                String stringContent = await delegateOutcome.Result.Content.ReadAsStringAsync(); // Could wrap this line in an additional policy as desired.
                if (delegateOutcome.Result.StatusCode == HttpStatusCode.InternalServerError && stringContent.Contains("SqlDateTime overflow"))
                {
                    throw new CustomSqlDateOverflowException(); // Replace 500 SqlDateTime overflow with something else.
                }
                else
                {
                    return delegateOutcome.Result; // render all other 500s as they were
                }
            }, async (delegateOutcome, context) => { /* log (if desired) that InternalServerError was checked for what kind */ });

        IAsyncPolicy<HttpResponseMessage> retryPolicy = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(r => r.StatusCode == HttpStatusCode.InternalServerError)
            .OrResult(r => /* condition for any other errors you want to handle */)
            .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                async (exception, timeSpan, context) =>
                {
                    /* log (if desired) retry being invoked */
                });

        HttpResponseMessage response = await retryPolicy.WrapAsync(rejectSqlError)
            .ExecuteAsync(() => client.PostAsync(requestUri, new StringContent(serialisedParameters, Encoding.UTF8, "application/json"), cancellationToken));

Если я правильно понимаю ваш вопрос, вы хотите повторить попытку, только если код состояния равен 500, а тело содержит Timeout. Если это так, вы можете определить свою политику так же, как это

      Policy<HttpResponseMessage>
    .HandleResult(response =>
        response.StatusCode == System.Net.HttpStatusCode.InternalServerError
        && response.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains("Timeout"))
    .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt);

Для Http я решил решить эту проблему, используя шаблон (DH) и polly. Здесь нетHandleResultAsync(), поэтому проблема все еще существует для обобщенного вопроса.

С Polly я избегаю решения, которое имеет «сцепление».

Я добился большого успеха с использованием политики повторных попыток в соответствии с SRP и обеспечивает хороший SoC (см. этот пост SO ). Вот повторная попытка DH, которую я обычно использую для справки.

Для вашего вопроса есть две вещи: повторная попытка и условия для повторной попытки. Опираясь на повторную попытку DH, я разделил ее на две части.DelegatingHandlers: повторная попытка DH, которая повторяет попытку по «сигналу», и последняя повторная попытка, сигнализирующая DH, которая сигнализирует о повторной попытке.HttpRequestMessageх.Properties(или.Options) сумка используется для подачи сигнала.

Я считаю, что его легко поддерживать, и он не сложен, поскольку позволяет избежать вложенных политик опроса или блокировки вызова. У меня есть несколько API-интерфейсов, использующих шаблон асинхронного запроса/ответа , поэтому повторная попытка DH (используемая для опроса) может быть повторно использована (нугетизирована), а повторная сигнализация DH отличается в зависимости от API. Очевидно, вы можете объединить их в один, встроив сигнальный код вactionаргумент

      HttpClient CoR (chain of responsibility):
... -> retry on signal DH -> retry signaling DH -> ...

Вот повторная попытка, сигнализирующая DH о ваших условиях для повторной попытки.

      public class RetrySignalingOnConditionHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);

        // tweak conditions accordingly
        if (response.StatusCode == (HttpStatusCode)500)
        {
            request.Properties[RequestProperties.RetrySignal] = true;
            return response;
        }
        var content = await response.Content.ReadAsStringAsync(cancellationToken);
        if (content.Contains("Timeout"))
        {
            request.Properties[RequestProperties.RetrySignal] = true;
            return response;
        }

        return response;
    }
}

internal static class RequestProperties
{
    internal static string RetrySignal = nameof(RetrySignal);
}

Вот повторная попытка DH, которая повторяет попытку сигнала. Он сбрасывает сигнал перед попыткой.

      public class ExponentialBackoffRetryOnSignalHandler : DelegatingHandler
{
    private readonly IAsyncPolicy<(HttpRequestMessage request, HttpResponseMessage response)> retryPolicy;

    public ExponentialBackoffRetryOnSignalHandler(
        IRetrySettings retrySettings)
    {
        _ = retrySettings
            ?? throw new ArgumentNullException(nameof(retrySettings));

        var sleepDurations = Backoff.ExponentialBackoff(
            initialDelay: TimeSpan.FromMilliseconds(retrySettings.RetryDelayInMilliseconds),
            retryCount: retrySettings.RetryCount);

        retryPolicy = Policy
            .HandleResult<(HttpRequestMessage request, HttpResponseMessage response)>(tuple =>
                tuple.request.Properties.TryGetValue(RequestProperties.RetrySignal, out var retrySignaledObj) && (bool)retrySignaledObj)
            .WaitAndRetryAsync(
                sleepDurations: sleepDurations,
                onRetry: (responseResult, delay, retryAttempt, context) =>
                {
                    // note: response can be null in case of handled exception
                    responseResult.Result.response?.Dispose();
                });
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var tuple = await retryPolicy.ExecuteAsync(
            action: async (ct) =>
            {
                request.Properties.Remove(RequestProperties.RetrySignal);
                var response = await base.SendAsync(request, ct)
                    .ConfigureAwait(false);
                return (request, response);
            },
            cancellationToken: cancellationToken)
                .ConfigureAwait(false);
        return tuple.response;
    }
}

public interface IRetrySettings
{
    int RetryCount { get; }
    int RetryDelayInMilliseconds { get; }
}

Вот полный код , который я использую вместе с тестами.

Я бы предложил использовать один класс DelegatingHandler, который выполняет одну IAsyncPolicy в методе SendAsync DelegatingHandler.

Затем тело ответа можно прочитать асинхронно в асинхронной лямбда-выражении, которая передается методу ExecuteAsync IAsyncPolicy.

      public class MyRetryDelegatingHandler : DelegatingHandler
{
    #region Fields and constructor
    private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;

    /// <param name="retryPolicy">The retryPolicy to execute during the SendAsync operation</param>
    public MyRetryDelegatingHandler(IAsyncPolicy<HttpResponseMessage> retryPolicy)
    {
        _retryPolicy = retryPolicy;
    }
    #endregion

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        // Execute retry policy
        var response = await _retryPolicy.ExecuteAsync(async () =>
        {
            // Send the HTTP request using base
            var response = await base.SendAsync(request, cancellationToken);

            if (response.StatusCode == HttpStatusCode.InternalServerError)
            {
                // Read the response body
                var content = await response.Content.ReadAsStringAsync();

                if (content is not null)
                {
                    // Check for timeout in response body
                    if (!content.Contains("Timeout"))
                    {
                        // Throw FlakyApiException if response was not a timeout error
                        throw new FlakyApiException(content);
                    }
                }
            }

            return response;
        });

        return response;
    }
}

Затем IAsyncPolicy можно создать в Program.cs следующим образом. IAsyncPolicy также можно создать в DelegatingHandler.

      // Create Polly policy
var teamsRetryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(500), retryCount: 5));

// Add TeamsRetryDelegatingHandler to services
builder.Services.AddTransient(provider => new MyRetryDelegatingHandler(teamsRetryPolicy));

// Add a typed HttpClient
// The extension method registers the typed client and type as transient services
builder.Services.AddHttpClient<IMyService>()
    .AddHttpMessageHandler<MyRetryDelegatingHandler>();

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

      public class MyService
{
    #region Fields and constructor
    private readonly HttpClient _httpClient;

    public MyService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    #endregion

    public async Task<HttpResponseMessage> SendApiRequest()
    {
        return await _httpClient.GetAsync("<url>");
    }
}

public interface IMyService
{
    Task<HttpResponseMessage> SendApiRequest();
}
Другие вопросы по тегам