From c33b299560419637abcf27698cd28ccdfadc4f04 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Sat, 13 Jan 2024 17:02:27 +0100 Subject: [PATCH] BreakDurationGeneratorArguments now includes half-open attempts --- .../BreakDurationGeneratorArguments.cs | 31 ++++++++++++++++ .../Controller/CircuitStateController.cs | 5 ++- src/Polly.Core/PublicAPI.Unshipped.txt | 2 ++ .../BreakDurationGeneratorArgumentsTests.cs | 29 ++++++--------- .../Controller/CircuitStateControllerTests.cs | 36 +++++++++++++++++++ 5 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/Polly.Core/CircuitBreaker/BreakDurationGeneratorArguments.cs b/src/Polly.Core/CircuitBreaker/BreakDurationGeneratorArguments.cs index 6b10a0e8d9d..8883f6dd774 100644 --- a/src/Polly.Core/CircuitBreaker/BreakDurationGeneratorArguments.cs +++ b/src/Polly.Core/CircuitBreaker/BreakDurationGeneratorArguments.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; + namespace Polly.CircuitBreaker; #pragma warning disable CA1815 // Override equals and operator equals on value types @@ -16,6 +18,7 @@ public readonly struct BreakDurationGeneratorArguments /// This count is used to determine if the failure threshold has been reached. /// The resilience context providing additional information /// about the execution state and failures. + [EditorBrowsable(EditorBrowsableState.Never)] public BreakDurationGeneratorArguments( double failureRate, int failureCount, @@ -24,6 +27,29 @@ public BreakDurationGeneratorArguments( FailureRate = failureRate; FailureCount = failureCount; Context = context; + HalfOpenAttempts = 0; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The failure rate at which the circuit breaker should trip. + /// It represents the ratio of failed actions to the total executed actions. + /// The number of failures that have occurred. + /// This count is used to determine if the failure threshold has been reached. + /// The resilience context providing additional information + /// about the execution state and failures. + /// The number of half-open attempts. + public BreakDurationGeneratorArguments( + double failureRate, + int failureCount, + ResilienceContext context, + int halfOpenAttempts) + { + FailureRate = failureRate; + FailureCount = failureCount; + Context = context; + HalfOpenAttempts = halfOpenAttempts; } /// @@ -40,4 +66,9 @@ public BreakDurationGeneratorArguments( /// Gets the context that provides additional information about the resilience operation. /// public ResilienceContext Context { get; } + + /// + /// Gets the number of half-open attempts. + /// + public int HalfOpenAttempts { get; } } diff --git a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs index e2333370884..12902f25c85 100644 --- a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs +++ b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs @@ -22,6 +22,7 @@ internal sealed class CircuitStateController : IDisposable private Outcome? _lastOutcome; private Exception? _breakingException; private bool _disposed; + private int _halfOpenAttempts; #pragma warning disable S107 public CircuitStateController( @@ -132,6 +133,7 @@ public ValueTask CloseCircuitAsync(ResilienceContext context) // check if circuit can be half-opened if (_circuitState == CircuitState.Open && PermitHalfOpenCircuitTest_NeedsLock()) { + _halfOpenAttempts++; _circuitState = CircuitState.HalfOpen; _telemetry.Report(new(ResilienceEventSeverity.Warning, CircuitBreakerConstants.OnHalfOpenEvent), context, new OnCircuitHalfOpenedArguments(context)); isHalfOpen = true; @@ -270,6 +272,7 @@ private void CloseCircuit_NeedsLock(Outcome outcome, bool manual, ResilienceC _blockedUntil = DateTimeOffset.MinValue; _lastOutcome = null; + _halfOpenAttempts = 0; CircuitState priorState = _circuitState; _circuitState = CircuitState.Closed; @@ -325,7 +328,7 @@ private void OpenCircuitFor_NeedsLock(Outcome outcome, TimeSpan breakDuration { #pragma warning disable CA2012 #pragma warning disable S1226 - breakDuration = _breakDurationGenerator(new(_behavior.FailureRate, _behavior.FailureCount, context)).GetAwaiter().GetResult(); + breakDuration = _breakDurationGenerator(new(_behavior.FailureRate, _behavior.FailureCount, context, _halfOpenAttempts)).GetAwaiter().GetResult(); #pragma warning restore S1226 #pragma warning restore CA2012 } diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index 6a64f8a1f95..14a878b70e0 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Polly.CircuitBreaker.BreakDurationGeneratorArguments.BreakDurationGeneratorArguments(double failureRate, int failureCount, Polly.ResilienceContext! context, int halfOpenAttempts) -> void +Polly.CircuitBreaker.BreakDurationGeneratorArguments.HalfOpenAttempts.get -> int Polly.Simmy.Behavior.BehaviorActionArguments Polly.Simmy.Behavior.BehaviorActionArguments.BehaviorActionArguments() -> void Polly.Simmy.Behavior.BehaviorActionArguments.BehaviorActionArguments(Polly.ResilienceContext! context) -> void diff --git a/test/Polly.Core.Tests/CircuitBreaker/BreakDurationGeneratorArgumentsTests.cs b/test/Polly.Core.Tests/CircuitBreaker/BreakDurationGeneratorArgumentsTests.cs index 5afa53fe6ae..7b0e5378559 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/BreakDurationGeneratorArgumentsTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/BreakDurationGeneratorArgumentsTests.cs @@ -6,7 +6,7 @@ namespace Polly.Core.Tests.CircuitBreaker; public class BreakDurationGeneratorArgumentsTests { [Fact] - public void Constructor_ShouldSetFailureRate() + public void Constructor_Old_Ok() { double expectedFailureRate = 0.5; int failureCount = 10; @@ -15,29 +15,22 @@ public void Constructor_ShouldSetFailureRate() var args = new BreakDurationGeneratorArguments(expectedFailureRate, failureCount, context); args.FailureRate.Should().Be(expectedFailureRate); + args.FailureCount.Should().Be(failureCount); + args.Context.Should().Be(context); } [Fact] - public void Constructor_ShouldSetFailureCount() + public void Constructor_Ok() { - double failureRate = 0.5; - int expectedFailureCount = 10; - var context = new ResilienceContext(); - - var args = new BreakDurationGeneratorArguments(failureRate, expectedFailureCount, context); - - args.FailureCount.Should().Be(expectedFailureCount); - } - - [Fact] - public void Constructor_ShouldSetContext() - { - double failureRate = 0.5; + double expectedFailureRate = 0.5; int failureCount = 10; - var expectedContext = new ResilienceContext(); + var context = new ResilienceContext(); - var args = new BreakDurationGeneratorArguments(failureRate, failureCount, expectedContext); + var args = new BreakDurationGeneratorArguments(expectedFailureRate, failureCount, context, 99); - args.Context.Should().Be(expectedContext); + args.FailureRate.Should().Be(expectedFailureRate); + args.FailureCount.Should().Be(failureCount); + args.Context.Should().Be(context); + args.HalfOpenAttempts.Should().Be(99); } } diff --git a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs index c7b086b0780..6ce574759e8 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs @@ -374,6 +374,41 @@ public async Task OnActionFailureAsync_EnsureBreakDurationGeneration() blockedTill.Should().Be(utcNow + TimeSpan.FromMinutes(42)); } + [Fact] + public async Task BreakDurationGenerator_EnsureHalfOpenAttempts() + { + // arrange + var halfOpenAttempts = new List(); + + _options.BreakDurationGenerator = args => + { + halfOpenAttempts.Add(args.HalfOpenAttempts); + return new ValueTask(TimeSpan.Zero); + }; + + using var controller = CreateController(); + + // act + await TransitionToState(controller, CircuitState.Closed); + + for (int i = 0; i < 5; i++) + { + await TransitionToState(controller, CircuitState.Open); + await TransitionToState(controller, CircuitState.HalfOpen); + } + + await TransitionToState(controller, CircuitState.Closed); + + for (int i = 0; i < 3; i++) + { + await TransitionToState(controller, CircuitState.Open); + await TransitionToState(controller, CircuitState.HalfOpen); + } + + // assert + halfOpenAttempts.Should().BeEquivalentTo([0, 1, 2, 3, 4, 0, 1, 2]); + } + [InlineData(true)] [InlineData(false)] [Theory] @@ -502,6 +537,7 @@ private async Task TransitionToState(CircuitStateController controller, Cir switch (state) { case CircuitState.Closed: + await controller.CloseCircuitAsync(ResilienceContextPool.Shared.Get()); break; case CircuitState.Open: await OpenCircuit(controller);