#c# #async-await #polly #circuit-breaker
#c# #асинхронный-ожидание #задача-параллельная-библиотека #polly #автоматический выключатель
Вопрос:
Я просто пробую Polly CircuitBreakerAsync, и он работает не так, как я ожидаю.
Что я здесь делаю не так? Я ожидаю, что приведенный ниже код завершится и скажет, что схема все еще замкнута.
using Polly;
using System;
using System.Threading.Tasks;
public class Program
{
public static void Main(string[] args)
{
MainAsync(args).GetAwaiter().GetResult();
}
static async Task MainAsync(string[] args)
{
var circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(
3, // ConsecutiveExceptionsAllowedBeforeBreaking,
TimeSpan.FromSeconds(5) // DurationOfBreak
);
Console.WriteLine("Circuit state before execution: " circuitBreaker.CircuitState);
await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
await circuitBreaker.ExecuteAsync(() => { throw new System.Exception(); });
await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
Console.WriteLine("Circuit state after execution: " circuitBreaker.CircuitState);
}
}
Скрипка: https://dotnetfiddle.net/unfKsC
Вывод:
Circuit state before execution: Closed
Run-time exception (line 25): Exception of type 'System.Exception' was thrown.
Stack Trace:
[System.Exception: Exception of type 'System.Exception' was thrown.]
at Program.<MainAsync>b__2() :line 25
at Polly.Policy.<>c__DisplayClass116_0.<ExecuteAsync>b__0(Context ctx, CancellationToken ct)
at Polly.CircuitBreakerSyntaxAsync.<>c__DisplayClass4_1.<<CircuitBreakerAsync>b__2>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Polly.CircuitBreaker.CircuitBreakerEngine.<ImplementationAsync>d__1`1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Polly.Policy.<ExecuteAsync>d__135.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Program.<MainAsync>d__a.MoveNext() :line 25
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Program.Main(String[] args) :line 9
Комментарии:
1. К вашему сведению
static async Task Main(string[] args)
, он был введен в C # 7.1.
Ответ №1:
Автоматический выключатель в целом
Ваш код работает так, как ожидалось. Сам автоматический выключатель не сломается, потому что вы установили количество последовательных ошибок равным 3. Это означает, что если у вас есть 3 последовательных неудачных вызова, то он перейдет из Closed
состояния Open
в. Если вы попытаетесь выполнить еще один вызов, он выдаст a BrokenCircuitException
. В Closed
состоянии, если было сгенерировано исключение и пороговое значение не было достигнуто, оно повторно генерирует исключение.
Я всегда предлагаю рассматривать автоматический выключатель в качестве прокси. Он разрешает вызовы, если все работает нормально. Если используемая подсистема / подкомпонент кажется неисправной, это предотвратит дальнейшие вызовы, чтобы избежать ненужной нагрузки.
Функции обратного вызова для отладки
Когда вы определяете политику автоматического отключения, вы можете указать 3 обратных вызова:
onBreak
: Когда он переходит изClosed
илиHalfOpen
вOpen
onReset
: Когда он переходит отHalfOpen
кClose
onHalfOpen
: Когда он переходит отOpen
кHalfOpen
Измененная декларация политики:
var circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(5),
onBreak: (ex, @break) => Console.WriteLine($"{"Break",-10}{@break,-10:ss\.fff}: {ex.GetType().Name}"),
onReset: () => Console.WriteLine($"{"Reset",-10}"),
onHalfOpen: () => Console.WriteLine($"{"HalfOpen",-10}")
);
Количество последовательных сбоев
Давайте изменим порог последовательного сбоя на 1 и давайте обернем ваши ExecuteAsync
вызовы в try catch:
var circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(1, TimeSpan.FromSeconds(5),
onBreak: (ex, @break) => Console.WriteLine($"{"Break",-10}{@break,-10:ss\.fff}: {ex.GetType().Name}"),
onReset: () => Console.WriteLine($"{"Reset",-10}"),
onHalfOpen: () => Console.WriteLine($"{"HalfOpen",-10}")
);
Console.WriteLine("Circuit state before execution: " circuitBreaker.CircuitState);
try
{
await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
await circuitBreaker.ExecuteAsync(() => { throw new System.Exception(); });
await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
}
catch (Exception ex)
{
Console.WriteLine("Circuit state after execution: " circuitBreaker.CircuitState);
Console.WriteLine(ex.GetType().Name);
}
Console.WriteLine("Circuit state after execution: " circuitBreaker.CircuitState);
Результат будет следующим:
Circuit state before execution: Closed
Break 05.000 : Exception
Circuit state after execution: Open
Exception
Как вы можете видеть, автоматический выключатель сломался и переходит из Closed
Open
состояния to. Он перестроил ваше исключение.
Объединить повторную попытку и автоматический выключатель
Чтобы легко продемонстрировать, когда CB выдает BrokenCircuitException
, я буду использовать логику повторных попыток вокруг CB.
var retry = Policy
.Handle<Exception>()
.Or<BrokenCircuitException>()
.WaitAndRetryAsync(
retryCount: 1,
sleepDurationProvider: _ => TimeSpan.FromSeconds(1),
onRetry: (exception, delay, context) =>
{
Console.WriteLine($"{"Retry",-10}{delay,-10:ss\.fff}: {exception.GetType().Name}");
});
Эта политика попытается повторно выполнить ваш делегат либо при Exception
вызове, либо при BrokenCircuitException
вызове. Это происходит с задержкой в 1 секунду между начальной попыткой и первой (и единственной) повторной попыткой.
Давайте объединим две политики и изменим ExecuteAsync
вызов:
var strategy = Policy.WrapAsync(retry, circuitBreaker);
try
{
await strategy.ExecuteAsync(() => { throw new System.Exception(); });
}
catch (Exception ex)
{
Console.WriteLine("Circuit state after execution: " circuitBreaker.CircuitState);
Console.WriteLine(ex.GetType().Name);
}
Результат будет следующим:
Circuit state before execution: Closed
Break 05.000 : Exception
Retry 01.000 : Exception
Circuit state after execution: Open
BrokenCircuitException
- Первоначальный вызов завершается неудачно, и он выдает
Exception
- CB прерывается, потому что достигнуто пороговое значение, и он повторно генерирует исключение
- Комбинированная политика приведет к эскалации проблемы с CB до повторной попытки
- Повторные попытки обрабатывают
Exception
, поэтому он ждет секунду, прежде чем снова попытается повторно выполнить делегат - Повторная попытка пытается снова вызвать делегата, но это не удается, потому что CB —
Open
вот почему aBrokenCircuitException
выбрасывается - Поскольку дальнейших повторных попыток нет, поэтому политика повторных попыток повторно создаст свое исключение (которое теперь является
BrokenCircuitException
экземпляром) - Это исключение перехватывается нашим
catch
блоком.
Точно настроенный пример
Давайте немного изменим параметры этих политик:
- CB
durationOfBreak
от 5 секунд до 1,5 - Повторите
retryCount
попытку с 1 по 2
var retry = Policy
.Handle<Exception>()
.Or<BrokenCircuitException>()
.WaitAndRetryAsync(2, _ => TimeSpan.FromSeconds(1),
onRetry: (exception, delay, context) =>
{
Console.WriteLine($"{"Retry",-10}{delay,-10:ss\.fff}: {exception.GetType().Name}");
});
var circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1500),
onBreak: (ex, @break) => Console.WriteLine($"{"Break",-10}{@break,-10:ss\.fff}: {ex.GetType().Name}"),
onReset: () => Console.WriteLine($"{"Reset",-10}"),
onHalfOpen: () => Console.WriteLine($"{"HalfOpen",-10}")
);
Console.WriteLine("Circuit state before execution: " circuitBreaker.CircuitState);
var strategy = Policy.WrapAsync(retry, circuitBreaker);
try
{
await strategy.ExecuteAsync(() => { throw new System.Exception(); });
}
catch (Exception ex)
{
Console.WriteLine("Circuit state after execution: " circuitBreaker.CircuitState);
Console.WriteLine(ex.GetType().Name);
}
Результат будет следующим:
Circuit state before execution: Closed
Break 01.500 : Exception
Retry 01.000 : Exception
Retry 01.000 : BrokenCircuitException
HalfOpen
Break 01.500 : Exception
Circuit state after execution: Open
Exception
Я надеюсь, что это небольшое демонстрационное приложение помогло вам лучше понять, как работает автоматический выключатель.
Комментарии:
1. Спасибо за этот исчерпывающий ответ, он очень полезен.
2. Я не понял вывод последнего примера… первые 3 строки я понимаю. Я не понимаю, почему 4-я строка не открыта наполовину.. сначала мы попали в исключение на разрыв — 2-я строка, затем подождали секунду для повторной попытки — 3-я строка. Теперь я ожидаю, что еще через 0,5 секунды мы достигли бы durationOfBreak, и теперь cb перейдет в режим полуоткрытия ПЕРЕД следующей повторной попыткой, у которой еще есть еще 0,5 секунды после этого. Почему это было не так?
3. @YonatanNir
onRetry
Обратный вызов вызывается ПЕРЕД переходом в спящий режим. Итак, он сообщает вам, что я попытаюсь выполнить новую попытку повтора через 1 секунду. Вот почему 3-я строка печатается раньшеHalfOpen
4. @YonatanNir Пожалуйста, взгляните на этот пример приложения здесь, я также использовал улучшенные отчеты для распечатки прошедшего времени. Я надеюсь, что это поможет вам получить ясность в отношении порядка событий.
5. @PeterCsala спасибо, я понял. Все еще есть исходный вопрос, на который вы мне ответили
Ответ №2:
Это работает так, как ожидалось
https://github.com/App-vNext/Polly/wiki/Circuit-Breaker
Обработка исключений
Автоматический выключатель существует как устройство измерения и отключения: для измерения обработанных исключений, вызванных действиями, которые вы размещаете через него, и для прерывания при превышении настроенного порога сбоя.
- Автоматический выключатель не организует повторные попытки.
- Автоматический выключатель (в отличие от повторной попытки) не поглощает исключения. Все исключения, создаваемые действиями, выполняемыми с помощью политики (как исключения, обрабатываемые политикой, так и нет), намеренно перестраиваются. Исключения, обрабатываемые метриками обновления политики, определяющими состояние схемы; исключения, не обрабатываемые политикой, этого не делают.
Короче говоря, он не обрабатывает ваши исключения, он их перестраивает
Комментарии:
1. Спасибо, я полностью упустил это из виду. RTFM 🙂