【问题标题】:Polly CircuitBreakerAsync is not working as I expectPolly CircuitBreakerAsync 没有按我的预期工作
【发布时间】:2021-02-28 03:36:45
【问题描述】:

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

【问题讨论】:

  • 仅供参考 static async Task Main(string[] args) 是在 C# 7.1 中引入的。

标签: c# async-await polly circuit-breaker


【解决方案1】:

这是按预期工作的

https://github.com/App-vNext/Polly/wiki/Circuit-Breaker

异常处理

断路器作为测量和分断设备存在: 测量您通过它放置的操作引发的已处理异常,以及 超过配置的故障阈值时中断。

  • 断路器不协调重试。
  • 断路器不会(与重试不同)吸收异常。 通过策略执行的操作引发的所有异常(两者 由策略处理而不是处理的异常)是故意的 重新抛出。政策更新指标处理的例外情况 电路状态;政策未处理的异常不会。

简而言之,它不会处理您的异常,而是重新抛出它们

【讨论】:

  • 谢谢,我完全忽略了这一点。 RTFM :)
【解决方案2】:

一般的断路器

您的代码按预期工作。断路器本身不会中断,因为您已将连续错误计数设置为 3。这意味着如果您有 3 次连续失败的调用,那么它将从Closed 状态转换为Open。如果您尝试执行另一个调用,那么它将抛出一个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 状态。它重新抛出了你的异常。

结合重试和断路器

为了方便演示 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,这就是抛出 BrokenCircuitException 的原因
  6. 因为没有进一步的重试,所以重试策略将重新抛出其异常(现在是 BrokenCircuitException 实例)
  7. 我们的 catch 块捕获了该异常。

微调示例

让我们稍微修改一下这些策略的参数:

  • CB 的 durationOfBreak 从 5 秒到 1.5
  • 从 1 到 2 重试 retryCount
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

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

【讨论】:

  • 感谢您的全面回答,非常有用。
  • 我不明白最后一个例子的输出......我明白前 3 行。我不明白为什么第 4 行不是半开的.. 我们首先在休息时出现异常 - 第 2 行,然后等待一秒钟以重试 - 第 3 行。现在我预计,再过 0.5 秒后,我们将达到 durationOfBreak 并且 cb 现在将在下一次重试之前转换为半开状态,此后仍有 0.5 秒。为什么不是这样?
  • @YonatanNir onRetry 回调在睡眠前被调用。所以它告诉你我将在 1 秒后尝试执行新的重试尝试。这就是为什么第三行打印在HalfOpen之前的原因
  • @YonatanNir 请看看这个sample application 这里我也使用了更好的报告来打印经过的时间。我希望它可以帮助您清楚事件的顺序。
  • @PeterCsala 谢谢我明白了。仍然得到你回答我的原始问题
猜你喜欢
  • 2015-11-17
  • 2010-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-08
  • 2021-01-01
相关资源
最近更新 更多