首页 > 解决方案 > Polly CircuitBreakerAsync 没有按我的预期工作

问题描述

我只是在试用 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

标签: c#async-awaitpollycircuit-breaker

解决方案


一般断路器

您的代码按预期工作。断路器本身不会中断,因为您已将连续错误计数设置为 3。这意味着如果您有 3 次连续失败的调用,那么它将从Closedstate 转换为Open. 如果您尝试执行另一个调用,那么它将抛出一个BrokenCircuitException. 在Closed状态下,如果已引发异常且未达到阈值,则它会重新引发异常。

我总是建议将断路器视为代理。如果一切正常,它允许调用。如果消耗的子系统/子组件似乎出现故障,那么它将阻止进一步调用以避免不必要的负载。

用于调试的回调函数

当您定义断路器策略时,您可以指定 3 个回调:

  • onBreak: 当它从Closed或转换HalfOpenOpen
  • onReset: 当它从 过渡HalfOpenClose
  • onHalfOpen: 当它从 过渡OpenHalfOpen

修改后的政策声明:

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状态。它重新抛出了你的异常。

结合重试和断路器

为了轻松演示 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此策略将尝试在抛出a 或抛出 a时重新执行您的委托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 升级到 Retry
  4. 重试处理Exception这就是为什么它在尝试再次重新执行委托之前等待一秒钟
  5. 重试尝试再次调用委托,但失败,因为 CB就是抛出Opena 的原因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

我希望这个小演示应用程序可以帮助您更好地了解断路器的工作原理。


推荐阅读