Skip to content

Commit

Permalink
Introduce API for Circuit Breaker Strategy (#1145)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk committed Apr 19, 2023
1 parent cec36cb commit 5bf2239
Show file tree
Hide file tree
Showing 36 changed files with 1,579 additions and 153 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.ComponentModel.DataAnnotations;
using Polly.CircuitBreaker;
using Polly.Strategy;
using Polly.Utils;
using Xunit;

namespace Polly.Core.Tests.CircuitBreaker;

public class AdvancedCircuitBreakerOptionsTests
{
[Fact]
public void Ctor_Defaults()
{
var options = new AdvancedCircuitBreakerStrategyOptions();

options.BreakDuration.Should().Be(TimeSpan.FromSeconds(5));
options.FailureThreshold.Should().Be(0.1);
options.MinimumThroughput.Should().Be(100);
options.SamplingDuration.Should().Be(TimeSpan.FromSeconds(30));
options.OnOpened.IsEmpty.Should().BeTrue();
options.OnClosed.IsEmpty.Should().BeTrue();
options.OnHalfOpened.IsEmpty.Should().BeTrue();
options.ShouldHandle.IsEmpty.Should().BeTrue();
options.StrategyType.Should().Be("CircuitBreaker");
options.StrategyName.Should().BeEmpty();

// now set to min values
options.FailureThreshold = 0.001;
options.BreakDuration = TimeSpan.FromMilliseconds(500);
options.MinimumThroughput = 2;
options.SamplingDuration = TimeSpan.FromMilliseconds(500);

ValidationHelper.ValidateObject(options, "Dummy.");
}

[Fact]
public void Ctor_Generic_Defaults()
{
var options = new AdvancedCircuitBreakerStrategyOptions<int>();

options.BreakDuration.Should().Be(TimeSpan.FromSeconds(5));
options.FailureThreshold.Should().Be(0.1);
options.MinimumThroughput.Should().Be(100);
options.SamplingDuration.Should().Be(TimeSpan.FromSeconds(30));
options.OnOpened.IsEmpty.Should().BeTrue();
options.OnClosed.IsEmpty.Should().BeTrue();
options.OnHalfOpened.IsEmpty.Should().BeTrue();
options.ShouldHandle.IsEmpty.Should().BeTrue();
options.StrategyType.Should().Be("CircuitBreaker");
options.StrategyName.Should().BeEmpty();

// now set to min values
options.FailureThreshold = 0.001;
options.BreakDuration = TimeSpan.FromMilliseconds(500);
options.MinimumThroughput = 2;
options.SamplingDuration = TimeSpan.FromMilliseconds(500);

ValidationHelper.ValidateObject(options, "Dummy.");
}

[Fact]
public async Task AsNonGenericOptions_Ok()
{
bool onBreakCalled = false;
bool onResetCalled = false;
bool onHalfOpenCalled = false;

var options = new AdvancedCircuitBreakerStrategyOptions<int>
{
BreakDuration = TimeSpan.FromSeconds(123),
FailureThreshold = 23,
SamplingDuration = TimeSpan.FromSeconds(124),
MinimumThroughput = 6,
StrategyType = "dummy-type",
StrategyName = "dummy-name",
OnOpened = new OutcomeEvent<OnCircuitOpenedArguments, int>().Register(() => onBreakCalled = true),
OnClosed = new OutcomeEvent<OnCircuitClosedArguments, int>().Register(() => onResetCalled = true),
OnHalfOpened = new NoOutcomeEvent<OnCircuitHalfOpenedArguments>().Register(() => onHalfOpenCalled = true),
ShouldHandle = new OutcomePredicate<CircuitBreakerPredicateArguments, int>().HandleException<InvalidOperationException>(),
ManualControl = new CircuitBreakerManualControl(),
StateProvider = new CircuitBreakerStateProvider()
};

var converted = options.AsNonGenericOptions();

// assert converted options
converted.StrategyType.Should().Be("dummy-type");
converted.StrategyName.Should().Be("dummy-name");
converted.FailureThreshold.Should().Be(23);
converted.BreakDuration.Should().Be(TimeSpan.FromSeconds(123));
converted.SamplingDuration.Should().Be(TimeSpan.FromSeconds(124));
converted.MinimumThroughput.Should().Be(6);
converted.ManualControl.Should().Be(options.ManualControl);
converted.StateProvider.Should().Be(options.StateProvider);

var context = ResilienceContext.Get();

(await converted.ShouldHandle.CreateHandler()!.ShouldHandleAsync(new Outcome<int>(new InvalidOperationException()), new CircuitBreakerPredicateArguments(context))).Should().BeTrue();

await converted.OnClosed.CreateHandler()!.HandleAsync(new Outcome<int>(new InvalidOperationException()), new OnCircuitClosedArguments(context));
onResetCalled.Should().BeTrue();

await converted.OnOpened.CreateHandler()!.HandleAsync(new Outcome<int>(new InvalidOperationException()), new OnCircuitOpenedArguments(context, TimeSpan.Zero));
onBreakCalled.Should().BeTrue();

await converted.OnHalfOpened.CreateHandler()!(new OnCircuitHalfOpenedArguments(context));
onHalfOpenCalled.Should().BeTrue();
}

[Fact]
public void InvalidOptions_Validate()
{
var options = new AdvancedCircuitBreakerStrategyOptions<int>
{
BreakDuration = TimeSpan.FromMilliseconds(299),
FailureThreshold = 0,
SamplingDuration = TimeSpan.Zero,
MinimumThroughput = 0,
OnOpened = null!,
OnClosed = null!,
OnHalfOpened = null!,
ShouldHandle = null!,
};

options
.Invoking(o => ValidationHelper.ValidateObject(o, "Dummy."))
.Should()
.Throw<ValidationException>()
.WithMessage("""
Dummy.

Validation Errors:
The field MinimumThroughput must be between 2 and 2147483647.
The field SamplingDuration must be >= to 00:00:00.5000000.
The field BreakDuration must be >= to 00:00:00.5000000.
The ShouldHandle field is required.
The OnClosed field is required.
The OnOpened field is required.
The OnHalfOpened field is required.
""");
}
}
51 changes: 51 additions & 0 deletions src/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Polly.CircuitBreaker;

namespace Polly.Core.Tests.CircuitBreaker;

public class BrokenCircuitExceptionTests
{
[Fact]
public void Ctor_Ok()
{
var brokenCircuit = new BrokenCircuitException();
new BrokenCircuitException("Dummy.").Message.Should().Be("Dummy.");
new BrokenCircuitException("Dummy.", new InvalidOperationException()).Message.Should().Be("Dummy.");
new BrokenCircuitException("Dummy.", new InvalidOperationException()).InnerException.Should().BeOfType<InvalidOperationException>();
}

[Fact]
public void Ctor_Generic_Ok()
{
var exception = new BrokenCircuitException<int>(10);
exception.Result.Should().Be(10);

exception = new BrokenCircuitException<int>("Dummy.", 10);
exception.Message.Should().Be("Dummy.");
exception.Result.Should().Be(10);

exception = new BrokenCircuitException<int>("Dummy.", new InvalidOperationException(), 10);
exception.Message.Should().Be("Dummy.");
exception.Result.Should().Be(10);
exception.InnerException.Should().BeOfType<InvalidOperationException>();
}

#if !NETCOREAPP
[Fact]
public void BinarySerialization_Ok()
{
BinarySerializationUtil.SerializeAndDeserializeException(new BrokenCircuitException()).Should().NotBeNull();
}

[Fact]
public void BinarySerialization_Generic_Ok()
{
var result = BinarySerializationUtil
.SerializeAndDeserializeException(new BrokenCircuitException<int>(123));

result.Should().NotBeNull();

// default
result.Result.Should().Be(0);
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using Polly.CircuitBreaker;

namespace Polly.Core.Tests.CircuitBreaker;

public class CircuitBreakerManualControlTests
{
[Fact]
public void Ctor_Ok()
{
var control = new CircuitBreakerManualControl();

control.IsInitialized.Should().BeFalse();
}

[Fact]
public async Task IsolateAsync_NotInitialized_Throws()
{
var control = new CircuitBreakerManualControl();

await control
.Invoking(c => c.IsolateAsync(CancellationToken.None))
.Should()
.ThrowAsync<InvalidOperationException>();
}

[Fact]
public async Task ResetAsync_NotInitialized_Throws()
{
var control = new CircuitBreakerManualControl();

await control
.Invoking(c => c.ResetAsync(CancellationToken.None))
.Should()
.ThrowAsync<InvalidOperationException>();
}

[Fact]
public void Initialize_Twice_Throws()
{
var control = new CircuitBreakerManualControl();
control.Initialize(_ => Task.CompletedTask, _ => Task.CompletedTask);

control
.Invoking(c => c.Initialize(_ => Task.CompletedTask, _ => Task.CompletedTask))
.Should()
.Throw<InvalidOperationException>();
}

[Fact]
public async Task Initialize_Ok()
{
var control = new CircuitBreakerManualControl();
var isolateCalled = false;
var resetCalled = false;

control.Initialize(
context =>
{
context.IsVoid.Should().BeTrue();
context.IsSynchronous.Should().BeFalse();
isolateCalled = true;
return Task.CompletedTask;
},
context =>
{
context.IsVoid.Should().BeTrue();
context.IsSynchronous.Should().BeFalse();
resetCalled = true;
return Task.CompletedTask;
});

await control.IsolateAsync(CancellationToken.None);
await control.ResetAsync(CancellationToken.None);

isolateCalled.Should().BeTrue();
resetCalled.Should().BeTrue();
}
}

0 comments on commit 5bf2239

Please sign in to comment.