如何将标准 CircuitBreaker 策略从 Polly V7 迁移到 V8?

问题描述 投票:0回答:1

到目前为止,我使用 Polly 的标准 CircuitBreaker 策略,如下所示:

    int retryCount = 2;

    PolicyBuilder policyBuilder = Policy
        .HandleInner<WebException>()
        .Or<WebException>()
        .Or<TimeoutException>()
        .Or<TimeoutRejectedException>();
    
    AsyncCircuitBreakerPolicy asyncCircuitBreakerPolicy = policyBuilder
        .CircuitBreakerAsync(1, TimeSpan.FromSeconds(1), OnBreak, OnReset);

    AsyncRetryPolicy asyncWaitAndRetry = policyBuilder
        .WaitAndRetryAsync(retryCount, SleepDurationProvider, OnRetry);

    AsyncPolicyWrap defaultPolicy = Policy.WrapAsync(
        asyncCircuitBreakerPolicy,
        asyncWaitAndRetry);

即抛出异常时重试 2 次,总共失败 3 次后,异常冒泡到立即触发的 CircuitBreaker。

Polly V8 不再有这种“标准”断路器,而是类似于旧的“高级”断路器。它希望最小吞吐量至少为 2,并且需要失败率而不是固定计数。此外,它会重新抛出所有异常。

现在我想知道如何迁移到V8。我正在考虑翻转重试和断路器的顺序,设置

MinimumThroughput = retryCount + 1
FailureRatio = 1
。但还有 SamplingDuration,所以我需要让它以某种方式取决于预期的超时,加上重试之间的等待时间等。

还有其他方法可以做到这一点吗?我应该编写自己的 ResilienceStrategy 吗?

.net polly circuit-breaker
1个回答
0
投票

在撰写本文时,V8 和 V7 之间有一个 互操作层(包装器)。不幸的是,它只能在一个方向上通过

.AsAsyncPolicy()
.AsSyncPolicy()
从 V8 到 V7。换句话说,目前没有内置支持将现有的 V7 策略包装到 V8 策略中。

那么,我们在这里能做什么?

选项#1

根据您的描述,使用 V8 的断路器有两个阻碍:

  • MinimumThroughput
    的最小值
  • 将正确的值设置为
    SamplingDuration

很高兴每当您将断路器切换到手动控制时,这些都不重要:

FailureRatio
MinimumThroughput
SamplingDuration
BreakDuration

换句话说,无论它们的值如何,您都可以要求断路器转换到

Isolated
状态或返回到
Closed

这里是如何做到这一点的示例代码

static void Report(string message) => Console.WriteLine($"{DateTime.UtcNow.TimeOfDay}:{message}");
const int MaxRetries = 2;

var manualControl = new CircuitBreakerManualControl();
var timer = new System.Timers.Timer(1500);
timer.Elapsed += async (s, e) => await manualControl.CloseAsync();

var pipeline = new ResiliencePipelineBuilder()
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        ManualControl = manualControl,
        OnOpened = static args =>
        {
            Report("Open");
            return default;
        },
        OnClosed = static args =>
        {
            Report("Close");
            return default;
        }
    })
    .AddRetry(new RetryStrategyOptions
    {
        ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
        MaxRetryAttempts = MaxRetries,
        Delay = TimeSpan.Zero,
        OnRetry = async args =>
        {
            if(args.AttemptNumber == MaxRetries -1)
            {
                await manualControl.IsolateAsync();
                timer.Start();
            }
        }
    })
    .Build();

for (int i = 0; i < 10; i++)
{
    try
    {
        await pipeline.ExecuteAsync((ct) => { Report("Called"); throw new SomeExceptionType(); }, CancellationToken.None);
    }
    catch(Exception ex)
    {
        Report(ex.GetType().Name);
        await Task.Delay(450);
    }
}
  • 我们创建了一个“空白”断路器,并通过
    manualControl
    添加了对其状态转换的手动控制。
  • 我们创建了一个重试策略来触发
    SomeExceptionType
    并最多重试
    MaxRetries
    次。
    • 在最后一次尝试之前,我们打破了电路
      我们还启动一个计时器,在一段时间后将断路器切换回
    • OnRetry
    • 
      
  • 这是一个示例输出:

Closed

这里有几个问题:

首先,我们在最后一次尝试之前而不是之后将断路器移至

13:04:55.2422830:Called 13:04:55.2486120:Called 13:04:55.2505380:Open 13:04:55.2523110:Called 13:04:55.2536750:SomeExceptionType 13:04:55.7084040:IsolatedCircuitException 13:04:56.1594240:IsolatedCircuitException 13:04:56.6119060:IsolatedCircuitException 13:04:56.7744830:Close 13:04:57.0636680:Called 13:04:57.0637870:Called 13:04:57.0638610:Open 13:04:57.0640420:Called 13:04:57.0654120:SomeExceptionType 13:04:57.5166330:IsolatedCircuitException 13:04:57.9679080:IsolatedCircuitException 13:04:58.2533750:Close 13:04:58.4195490:Called 13:04:58.4199850:Called 13:04:58.4218660:Open 13:04:58.4220250:Called 13:04:58.4244720:SomeExceptionType 13:04:58.8763750:IsolatedCircuitException 13:04:59.3277210:IsolatedCircuitException 13:04:59.7532630:Close

状态。这是因为

Isolated
在下一次重试尝试之前运行。解决这个问题的一种方法是将
OnRetry
的逻辑移到 catch 块中
OnRetry

现在示例输出如下所示:

... var pipeline = new ResiliencePipelineBuilder() .AddCircuitBreaker(new CircuitBreakerStrategyOptions { ManualControl = manualControl, OnOpened = static args => { Report("Open"); return default; }, OnClosed = args => { Report("Close"); return default; } }) .AddRetry(new RetryStrategyOptions { ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(), MaxRetryAttempts = MaxRetries, Delay = TimeSpan.Zero, }) .Build(); for (int i = 0; i < 10; i++) { try { await pipeline.ExecuteAsync(_ => { Report("Called"); throw new SomeExceptionType(); }, CancellationToken.None); } catch(SomeExceptionType ex) { await manualControl.IsolateAsync(); timer.Start(); } catch (Exception ex) { Report(ex.GetType().Name); } await Task.Delay(450); }

第二个问题,也是更大的问题:断路器的控制代码分散在各处。很难将当前的解决方案封装成可重用的格式。

选项#2

因为您的断路器应该在重试次数用完后立即断开,所以您可以自己简单地实现 CB 部分。这是一个例子:

13:09:21.2632760:Called 13:09:21.2694840:Called 13:09:21.2695320:Called 13:09:21.2724070:Open 13:09:21.7255970:IsolatedCircuitException 13:09:22.1786920:IsolatedCircuitException 13:09:22.6303060:IsolatedCircuitException 13:09:22.8046920:Close 13:09:23.0817970:Called 13:09:23.0821610:Called 13:09:23.0822510:Called 13:09:23.0826860:Open 13:09:23.5354960:IsolatedCircuitException 13:09:23.9883700:IsolatedCircuitException 13:09:24.2743980:Close 13:09:24.4395530:Called 13:09:24.4398200:Called 13:09:24.4398690:Called 13:09:24.4401100:Open 13:09:24.8934820:IsolatedCircuitException 13:09:25.3463760:IsolatedCircuitException 13:09:25.7748690:Close

这里我们基本上模仿一个原子布尔标志来捕获断路器是否处于闭合或打开状态。

如果您运行这样的测试

public class Foo { static readonly ResiliencePipeline pipeline = new ResiliencePipelineBuilder() .AddRetry(new RetryStrategyOptions { ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(), MaxRetryAttempts = 2, Delay = TimeSpan.Zero, }) .Build(); const int NotAllowed = 0; const int Allowed = 1; private static int isAllowed = Allowed; public async ValueTask Bar(Func<ValueTask> callback) { if(isAllowed == NotAllowed) throw new BrokenCircuitException(); try { await pipeline.ExecuteAsync(_ => callback(), CancellationToken.None); } catch(SomeExceptionType) { Report("Open"); Interlocked.Exchange(ref isAllowed, NotAllowed); System.Timers.Timer timer = new (1500); timer.Elapsed += (s, e) => { Report("Close"); Interlocked.Exchange(ref isAllowed, Allowed); }; timer.Start(); } } }

那么输出将是这样的:

var foo = new Foo(); for (int i = 0; i < 10; i++) { try { await foo.Bar(() => { Report("Called"); throw new SomeExceptionType();}); } catch(BrokenCircuitException) { Report("Broken"); await Task.Delay(450); } }

这里核心逻辑被封装并且可以复用。但它不能与其他弹性策略组合。

因此,如果您想实现这种特定的弹性逻辑,那么有很多选择。如果您需要进行撰写,那么您必须实现自定义
13:50:50.2469980:Called 13:50:50.2539870:Called 13:50:50.2540370:Called 13:50:50.2546650:Open 13:50:50.2552080:Broken 13:50:50.7074720:Broken 13:50:51.1621140:Broken 13:50:51.6145440:Broken 13:50:51.7849980:Close 13:50:52.0653560:Called 13:50:52.0657160:Called 13:50:52.0658150:Called 13:50:52.0660950:Open 13:50:52.0663610:Broken 13:50:52.5180710:Broken 13:50:52.9704980:Broken 13:50:53.2559450:Close 13:50:53.4215020:Called 13:50:53.4221810:Called 13:50:53.4222580:Called 13:50:53.4225390:Open

    

© www.soinside.com 2019 - 2024. All rights reserved.