Polly CircuitBreakerAsync работает не так, как я ожидаю

#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
  
  1. Первоначальный вызов завершается неудачно, и он выдает Exception
  2. CB прерывается, потому что достигнуто пороговое значение, и он повторно генерирует исключение
  3. Комбинированная политика приведет к эскалации проблемы с CB до повторной попытки
  4. Повторные попытки обрабатывают Exception , поэтому он ждет секунду, прежде чем снова попытается повторно выполнить делегат
  5. Повторная попытка пытается снова вызвать делегата, но это не удается, потому что CB — Open вот почему a BrokenCircuitException выбрасывается
  6. Поскольку дальнейших повторных попыток нет, поэтому политика повторных попыток повторно создаст свое исключение (которое теперь является BrokenCircuitException экземпляром)
  7. Это исключение перехватывается нашим 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 🙂