Используя Polly v8 и RestSharp, как мне построить Generic ResiliencePipeline для учета повторных попыток заголовка ответа, исключений и ведения журнала?

У меня есть клиентский класс API, созданный с использованием _client = new RestSharp.RestClient(...); с методами этой формы:

public async Task<TResponse> PostAsync<TRequest, TResponse>(string resource, TRequest payload) where TRequest : class
{
    var request = new RestRequest(resource);
    request.AddJsonBody(payload);
    var response = await PolicyFactory.RestSharpPolicy<B2BResponse<TResponse>>(_logger).ExecuteAsync(
        async token => await _client.ExecuteAsync<B2BResponse<TResponse>>(request, Method.Post, token));
    LogAndRaiseErrors(request, response);
    return response.Data.Data;
}

Вот метод LogAndRaiseErrors:

protected void LogAndRaiseErrors(RestRequest request, RestResponse response)
{
    if (response.StatusCode != HttpStatusCode.OK)
    {
        _logger.LogError("API Error:{response}", SerializeResponse(response));
        throw new BigCommerceException(response.Content);
    }
    _logger.LogDebug("B2B API Call:\n{response}", SerializeResponse(response));
} 

Я просмотрел документацию Polly, но она немного скудна.
Как бы я построил ResilencePipeline, используемый в методе PostAsync, для достижения следующих основных целей:

  • Прочитайте response.Headers.FirstOrDefault(h=>h.Name?.ToLower() == "retry-after")?.Value и задержите на указанные секунды, чего существующий код вообще не делает.
  • Я считаю, что повторная попытка должна влиять на все потоки, которые могут вызывать данный API? добавить автоматический выключатель?

Любые указатели на второстепенные цели были бы полезны:

  • Регистрируйте каждый запрос (в настоящее время это делается в LogAndRaiseErrorsMethod).
  • Записывать в журнал повторные попытки, включая задержку, указанную в заголовке.
  • Переместите LogAndRaiseErrors(...) в политику, чтобы политика могла обрабатывать журналирование и любые исключения.
  • Добавьте политику повторных попыток в случае сетевых сбоев/тайм-аутов и т. д.
  • Я подозреваю, что нам нужен некоторый джиттер (для этого ли он нужен?), чтобы не все потоки одновременно загружали API?

ОБНОВЛЕНИЕ, вот что я пробовал:
Я думаю, что повторная попытка работает правильно, но не автоматический выключатель. Я перенес создание конвейера в конструктор, поэтому существует только один конвейер (журнал показывает, что он вызывается один раз).

public ApiClient(..., ILogger<B2BClient> logger)
{
    ...
    _client = client;
    _resiliencePipeline = PolicyFactory.GetRestSharpPolicy(_logger);
}

public async Task<TResponse> GetAsync<TResponse>(string resource)
{...}

public async Task<TResponse> PostAsync<TRequest, TResponse>(string resource, TRequest payload) where TRequest : class
{
    var request = new RestRequest(resource);
    request.AddJsonBody(payload);
    var response = await _resiliencePipeline.ExecuteAsync(
        async token => await _client.ExecuteAsync<BigCommerceResponse<TResponse>>(request, Method.Post, token));
    LogAndRaiseErrors(request, response);
    return response.Data.Data;
}

Вот мое создание конвейера, но я никогда не вижу записей в журнале прерывателя цепи, но вижу много записей в журнале повторных попыток.

public static class PolicyFactory
{

    public static ResiliencePipeline<RestResponse> GetRestSharpPolicy(ILogger logger)
    {
        logger.LogInformation("Building ResiliencePipeline");
        return new ResiliencePipelineBuilder<RestResponse>()
            .AddCircuitBreaker(new CircuitBreakerStrategyOptions<RestResponse>
            {
                FailureRatio = 0,
                ShouldHandle = new PredicateBuilder<RestResponse>()
                    .HandleResult(static result => result.StatusCode == HttpStatusCode.TooManyRequests),
            
                OnOpened = args =>
                {
                    logger.LogWarning("Circuit Breaker Opened on {StatusCode} for {Duration}s ({ResponseUri})",
                        args.Outcome.Result.StatusCode, args.BreakDuration.TotalSeconds, args.Outcome.Result.ResponseUri);
                    return ValueTask.CompletedTask;
                },
                OnClosed = args =>
                {
                    logger.LogWarning("Circuit Breaker Closed on {StatusCode} ({ResponseUri})",
                        args.Outcome.Result.StatusCode, args.Outcome.Result.ResponseUri);
                    return ValueTask.CompletedTask;
                }
            })
            .AddRetry(new RetryStrategyOptions<RestResponse>
            {
                ShouldHandle = new PredicateBuilder<RestResponse>()
                    .HandleResult(static result => result.StatusCode == HttpStatusCode.TooManyRequests),
                DelayGenerator = delayArgs =>
                {
                    var retryAfter = delayArgs.Outcome.Result.Headers.FirstOrDefault(h => h.Name?.ToLower() == "retry-after")?.Value.ToString();
                    return int.TryParse(retryAfter, out var seconds)
                        ? new ValueTask<TimeSpan?>(TimeSpan.FromSeconds(seconds))
                        : new ValueTask<TimeSpan?>(TimeSpan.FromSeconds(0.5));
                },
                MaxRetryAttempts = 5,
                OnRetry = args =>
                {
                    logger.LogWarning("Retry Attempt:{Attempt} Delay:{Delay}",
                        args.AttemptNumber, args.RetryDelay);
                    return ValueTask.CompletedTask;
                }
            })
            .Build();
    }
}

Просто чтобы уточнить: вы спрашиваете, как создать конвейер устойчивости со стратегией повторных попыток, которая учитывает заголовок RetryAfter? + протоколирование. ИЛИ вы хотите получить ответы на ваши конкретные вопросы?

Peter Csala 10.07.2024 09:57

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

Myster 10.07.2024 22:48

Привет @PeterCsala, спасибо за интерес, я обновил свой вопрос тем, что пробовал.

Myster 11.07.2024 01:58
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть несколько мелких вещей, которые нужно исправить, чтобы все заработало.

Порядок регистрации

Существует огромная разница между следующими двумя конвейерами.

var pipeline = new ResiliencePipelineBuilder()
    .AddCircuitBreaker( new() { ... }) // outer
    .AddRetry(new() { ... }) // inner
    .Build();
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new() { ... }) // outer
    .AddCircuitBreaker( new() { ... }) // inner
    .Build();

Есть такое понятие, как эскалация. Если внутренняя стратегия не обрабатывает ответ/исключение, она передает результат следующей стратегии в цепочке.

Итак, если вы определите в своем конвейере, что внутренняя стратегия обрабатывает код состояния 429, тогда внешняя стратегия никогда не сработает. Вот почему вам придется изменить порядок и переместить CB самым внутренним.

Мы создали специальный раздел на сайте pollydoc, чтобы проиллюстрировать различия между порядками регистрации.

Настройки автоматического выключателя

Имейте в виду, что значения по умолчанию для CB подходят для производственной нагрузки, а не для тестирования.

Свойство Значение по умолчанию Описание FailureRatio 0,1 Отношение неудач к успехам, которое приведет к разрыву/размыканию цепи. 0,1 означает, что 10 % из всех выборочных выполнений оказались неудачными. MinimumThroughput 100 Минимальное количество выполнений, которое должно произойти в течение указанного периода выборки. SamplingDuration 30 секунд Период времени, за который рассчитывается соотношение неудач и успехов.

Это означает, что в течение 30 секунд у вас должно быть не менее 100 запросов и не менее 10 неудачных попыток разомкнуть цепь.

FailureRatio следует читать так

  • 0: Одного неудачного запроса достаточно, чтобы цепь разомкнулась.
  • 1: 100 запросов не должны открыть цепь
  • 0.2: 20 запросов не должны открыть цепь

Я не уверен, что 0 был намеренным или нет. Если нет, то я бы предложил поиграть с этим кодом (с разными значениями FailureRatio и разными значениями больше), чтобы лучше понять, как работает CB:

var pipeline = new ResiliencePipelineBuilder<int>()
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions<int>
    {
        FailureRatio = 1, //0, 0.2, 0.5
        ShouldHandle = new PredicateBuilder<int>().HandleResult(result => result > 100), // 50, 100, 150, 190
        OnOpened = static args => { Console.WriteLine("Opened"); return default; },
        OnClosed = static args => { Console.WriteLine("Closed"); return default; }
    }).Build();

for(int i = 1; i < 200; i++)
{
    try
    {
       _ = pipeline.Execute(() => i);
    }
    catch(BrokenCircuitException)
    {
        Console.WriteLine($"{i}: BCE");
    }
}

Повторить попытку через

В вашем решении вы используете заголовок RetryAfter только для повторной попытки. Но вы можете использовать это и для автоматического выключателя.

По умолчанию ЦБ ломается на 5 секунд. Но вы можете динамически устанавливать продолжительность перерыва с помощью BreakDurationGenerator.

Это приведет к сокращению любых исходящих запросов с помощью BrokenCircuitException до тех пор, пока не истечет RetryAfter. Для обработки BrokenCircuitException это также потребует некоторых изменений в вашей стратегии повторных попыток.

Также имейте в виду, что RetryAfter cможет содержать не только значение дельты, но и дату и время. Это означает, что ваш TimeSpan.FromSeconds может потерпеть неудачу, если повторная попытка после будет установлена ​​следующим образом: Retry-After: Wed, 10 Jul 2024 09:35:00 GMT


ОБНОВЛЕНИЕ №1

Я не знаю, как мне получить и поделиться задержкой, у меня нет доступа к args.Outcome в BreakDurationGenerator

Хммм, это странно. Я думал, мы добавили Результат в BreakDurationGeneratorArguments. С радостью кажется, это легко исправить На этой неделе я сделаю пиар. Добавление Outcome с обратной совместимостью кажется довольно сложной задачей. Я ищу другую альтернативу.

Как лучше всего сообщить повторной попытке о той же задержке, что и у CircuitBreaker... Должен ли я отказаться от автоматического выключателя и просто использовать повторную попытку? я слишком усложняю ситуацию?

Предлагаемый способ — использовать контекст для обмена информацией между несколькими стратегиями. Здесь можно посмотреть подробности.

Нулевой коэффициент отказов был преднамеренным. Я думаю, что если сервер когда-нибудь скажет, что вы отправляете слишком много запросов, мне всегда следует открывать канал, пока не истечет предложенное время ожидания. Исходя из этого, я считаю, что повторная попытка после этого должна обрабатывать только BrokenCircuitException? Думаю, все остальное, что вы сказали, имеет смысл, попробую в понедельник. Спасибо за помощь.

Myster 11.07.2024 22:59

@Myster Нет, ваша повторная попытка должна обрабатывать как 429, так и BrokenCircuitException. При первом же ответе 429 ЦБ сломается, но также передаст ответ внешней стратегии. Сокращаются только последующие запросы. Также имейте в виду, что наименьшее значение MinimumThroughput — 2.

Peter Csala 12.07.2024 08:54

Привет @PeterCsala. У меня есть пара ошибок после переупорядочения конвейера и установки MinimumThroughput на 2; 1) logWarnings в OnOpened не выводятся. (но отладка показывает, что они поражены, тогда, вероятно, это не проблема опроса) 2) Я не уверен, как мне следует получить и поделиться задержкой, у меня нет доступа к args.Outcome в BreakDurationGenerator 3) Каков наилучший способ сообщите Retry ту же задержку, что и у CircuitBreaker... Должен ли я отказаться от автоматического выключателя и просто использовать повторную попытку? я слишком усложняю ситуацию?

Myster 16.07.2024 04:32

@Myster Я обновил свой пост, чтобы отразить ваши вопросы.

Peter Csala 16.07.2024 10:00

Привет @PeterCsala. Я очень ценю твою помощь, спасибо! Я думаю, что сейчас я упрощу ситуацию, оставив автоматический выключатель, это будет означать еще пару запросов 429, но я думаю, что простота кода на данный момент того стоит. Возможно, мне придется вернуться к нему, когда мы попытаемся увеличить объем, поэтому буду рад обновлению вашего ответа, если это произойдет :-)

Myster 16.07.2024 23:32

Другие вопросы по теме