Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce API for Circuit Breaker Strategy #1145

Merged
merged 8 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
""");
}
}
43 changes: 43 additions & 0 deletions src/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 brokenCircuit = new BrokenCircuitException<int>(10).Result.Should().Be(10);
new BrokenCircuitException<int>("Dummy.", 10).Message.Should().Be("Dummy.");
new BrokenCircuitException<int>("Dummy.", 10).Result.Should().Be(10);
}

#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();
}
}
127 changes: 127 additions & 0 deletions src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerOptionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.ComponentModel.DataAnnotations;
using Polly.CircuitBreaker;
using Polly.Strategy;
using Polly.Utils;
using Xunit;

namespace Polly.Core.Tests.CircuitBreaker;

public class CircuitBreakerOptionsTests
{
[Fact]
public void Ctor_Defaults()
{
var options = new CircuitBreakerStrategyOptions();

options.BreakDuration.Should().Be(TimeSpan.FromSeconds(5));
options.FailureThreshold.Should().Be(100);
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 = 1;
options.BreakDuration = TimeSpan.FromMilliseconds(500);

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

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

options.BreakDuration.Should().Be(TimeSpan.FromSeconds(5));
options.FailureThreshold.Should().Be(100);
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 = 1;
options.BreakDuration = 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 CircuitBreakerStrategyOptions<int>
{
BreakDuration = TimeSpan.FromSeconds(123),
FailureThreshold = 23,
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.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 CircuitBreakerStrategyOptions<int>
{
BreakDuration = TimeSpan.FromMilliseconds(299),
FailureThreshold = 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 FailureThreshold must be between 1 and 2147483647.
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.
""");
}
}
Loading