У меня есть клиентский класс 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
и задержите на указанные секунды, чего существующий код вообще не делает.Любые указатели на второстепенные цели были бы полезны:
LogAndRaiseErrors(...)
в политику, чтобы политика могла обрабатывать журналирование и любые исключения.ОБНОВЛЕНИЕ, вот что я пробовал:
Я думаю, что повторная попытка работает правильно, но не автоматический выключатель.
Я перенес создание конвейера в конструктор, поэтому существует только один конвейер (журнал показывает, что он вызывается один раз).
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();
}
}
Я думаю, что у меня слишком много всего происходит в одном вопросе, поэтому давайте придерживаться того, что вы говорите, но если кто-то знает, с чего начать по другим вещам, было бы здорово... Также я думаю, что мне нужна эта повторная попытка, чтобы следить за автоматическим выключателем шаблон, чтобы другие потоки не использовали свои повторные попытки преждевременно.
Привет @PeterCsala, спасибо за интерес, я обновил свой вопрос тем, что пробовал.
Есть несколько мелких вещей, которые нужно исправить, чтобы все заработало.
Существует огромная разница между следующими двумя конвейерами.
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 Нет, ваша повторная попытка должна обрабатывать как 429, так и BrokenCircuitException
. При первом же ответе 429 ЦБ сломается, но также передаст ответ внешней стратегии. Сокращаются только последующие запросы. Также имейте в виду, что наименьшее значение MinimumThroughput
— 2.
Привет @PeterCsala. У меня есть пара ошибок после переупорядочения конвейера и установки MinimumThroughput
на 2; 1) logWarnings в OnOpened не выводятся. (но отладка показывает, что они поражены, тогда, вероятно, это не проблема опроса) 2) Я не уверен, как мне следует получить и поделиться задержкой, у меня нет доступа к args.Outcome
в BreakDurationGenerator
3) Каков наилучший способ сообщите Retry ту же задержку, что и у CircuitBreaker... Должен ли я отказаться от автоматического выключателя и просто использовать повторную попытку? я слишком усложняю ситуацию?
@Myster Я обновил свой пост, чтобы отразить ваши вопросы.
Привет @PeterCsala. Я очень ценю твою помощь, спасибо! Я думаю, что сейчас я упрощу ситуацию, оставив автоматический выключатель, это будет означать еще пару запросов 429, но я думаю, что простота кода на данный момент того стоит. Возможно, мне придется вернуться к нему, когда мы попытаемся увеличить объем, поэтому буду рад обновлению вашего ответа, если это произойдет :-)
Просто чтобы уточнить: вы спрашиваете, как создать конвейер устойчивости со стратегией повторных попыток, которая учитывает заголовок RetryAfter? + протоколирование. ИЛИ вы хотите получить ответы на ваши конкретные вопросы?