Skip to content

Commit

Permalink
BreakDurationGeneratorArguments now includes half-open attempts (#1898)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Jan 13, 2024
1 parent ce95e74 commit fd1b310
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 19 deletions.
31 changes: 31 additions & 0 deletions src/Polly.Core/CircuitBreaker/BreakDurationGeneratorArguments.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.ComponentModel;

namespace Polly.CircuitBreaker;

#pragma warning disable CA1815 // Override equals and operator equals on value types
Expand All @@ -16,6 +18,7 @@ public readonly struct BreakDurationGeneratorArguments
/// This count is used to determine if the failure threshold has been reached.</param>
/// <param name="context">The resilience context providing additional information
/// about the execution state and failures.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public BreakDurationGeneratorArguments(
double failureRate,
int failureCount,
Expand All @@ -24,6 +27,29 @@ public BreakDurationGeneratorArguments(
FailureRate = failureRate;
FailureCount = failureCount;
Context = context;
HalfOpenAttempts = 0;
}

/// <summary>
/// Initializes a new instance of the <see cref="BreakDurationGeneratorArguments"/> struct.
/// </summary>
/// <param name="failureRate">The failure rate at which the circuit breaker should trip.
/// It represents the ratio of failed actions to the total executed actions.</param>
/// <param name="failureCount">The number of failures that have occurred.
/// This count is used to determine if the failure threshold has been reached.</param>
/// <param name="context">The resilience context providing additional information
/// about the execution state and failures.</param>
/// <param name="halfOpenAttempts">The number of half-open attempts.</param>
public BreakDurationGeneratorArguments(
double failureRate,
int failureCount,
ResilienceContext context,
int halfOpenAttempts)
{
FailureRate = failureRate;
FailureCount = failureCount;
Context = context;
HalfOpenAttempts = halfOpenAttempts;
}

/// <summary>
Expand All @@ -40,4 +66,9 @@ public BreakDurationGeneratorArguments(
/// Gets the context that provides additional information about the resilience operation.
/// </summary>
public ResilienceContext Context { get; }

/// <summary>
/// Gets the number of half-open attempts.
/// </summary>
public int HalfOpenAttempts { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal sealed class CircuitStateController<T> : IDisposable
private Outcome<T>? _lastOutcome;
private Exception? _breakingException;
private bool _disposed;
private int _halfOpenAttempts;

#pragma warning disable S107
public CircuitStateController(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -270,6 +272,7 @@ private void CloseCircuit_NeedsLock(Outcome<T> outcome, bool manual, ResilienceC

_blockedUntil = DateTimeOffset.MinValue;
_lastOutcome = null;
_halfOpenAttempts = 0;

CircuitState priorState = _circuitState;
_circuitState = CircuitState.Closed;
Expand Down Expand Up @@ -325,7 +328,7 @@ private void OpenCircuitFor_NeedsLock(Outcome<T> 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
}
Expand Down
2 changes: 2 additions & 0 deletions src/Polly.Core/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>();

_options.BreakDurationGenerator = args =>
{
halfOpenAttempts.Add(args.HalfOpenAttempts);
return new ValueTask<TimeSpan>(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]
Expand Down Expand Up @@ -502,6 +537,7 @@ private async Task TransitionToState(CircuitStateController<int> controller, Cir
switch (state)
{
case CircuitState.Closed:
await controller.CloseCircuitAsync(ResilienceContextPool.Shared.Get());
break;
case CircuitState.Open:
await OpenCircuit(controller);
Expand Down

0 comments on commit fd1b310

Please sign in to comment.