diff --git a/src/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs b/src/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs new file mode 100644 index 00000000000..5bb613e6b7b --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs @@ -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(); + + 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 + { + BreakDuration = TimeSpan.FromSeconds(123), + FailureThreshold = 23, + SamplingDuration = TimeSpan.FromSeconds(124), + MinimumThroughput = 6, + StrategyType = "dummy-type", + StrategyName = "dummy-name", + OnOpened = new OutcomeEvent().Register(() => onBreakCalled = true), + OnClosed = new OutcomeEvent().Register(() => onResetCalled = true), + OnHalfOpened = new NoOutcomeEvent().Register(() => onHalfOpenCalled = true), + ShouldHandle = new OutcomePredicate().HandleException(), + 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(new InvalidOperationException()), new CircuitBreakerPredicateArguments(context))).Should().BeTrue(); + + await converted.OnClosed.CreateHandler()!.HandleAsync(new Outcome(new InvalidOperationException()), new OnCircuitClosedArguments(context)); + onResetCalled.Should().BeTrue(); + + await converted.OnOpened.CreateHandler()!.HandleAsync(new Outcome(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 + { + 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() + .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. + """); + } +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs b/src/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs new file mode 100644 index 00000000000..f29fe8d8dc0 --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs @@ -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(); + } + + [Fact] + public void Ctor_Generic_Ok() + { + var exception = new BrokenCircuitException(10); + exception.Result.Should().Be(10); + + exception = new BrokenCircuitException("Dummy.", 10); + exception.Message.Should().Be("Dummy."); + exception.Result.Should().Be(10); + + exception = new BrokenCircuitException("Dummy.", new InvalidOperationException(), 10); + exception.Message.Should().Be("Dummy."); + exception.Result.Should().Be(10); + exception.InnerException.Should().BeOfType(); + } + +#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(123)); + + result.Should().NotBeNull(); + + // default + result.Result.Should().Be(0); + } +#endif +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerManualControlTests.cs b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerManualControlTests.cs new file mode 100644 index 00000000000..a16ceebf55a --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerManualControlTests.cs @@ -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(); + } + + [Fact] + public async Task ResetAsync_NotInitialized_Throws() + { + var control = new CircuitBreakerManualControl(); + + await control + .Invoking(c => c.ResetAsync(CancellationToken.None)) + .Should() + .ThrowAsync(); + } + + [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(); + } + + [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(); + } +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerOptionsTests.cs b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerOptionsTests.cs new file mode 100644 index 00000000000..d1d20b43782 --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerOptionsTests.cs @@ -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(); + + 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 + { + BreakDuration = TimeSpan.FromSeconds(123), + FailureThreshold = 23, + StrategyType = "dummy-type", + StrategyName = "dummy-name", + OnOpened = new OutcomeEvent().Register(() => onBreakCalled = true), + OnClosed = new OutcomeEvent().Register(() => onResetCalled = true), + OnHalfOpened = new NoOutcomeEvent().Register(() => onHalfOpenCalled = true), + ShouldHandle = new OutcomePredicate().HandleException(), + 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(new InvalidOperationException()), new CircuitBreakerPredicateArguments(context))).Should().BeTrue(); + + await converted.OnClosed.CreateHandler()!.HandleAsync(new Outcome(new InvalidOperationException()), new OnCircuitClosedArguments(context)); + onResetCalled.Should().BeTrue(); + + await converted.OnOpened.CreateHandler()!.HandleAsync(new Outcome(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 + { + BreakDuration = TimeSpan.FromMilliseconds(299), + FailureThreshold = 0, + OnOpened = null!, + OnClosed = null!, + OnHalfOpened = null!, + ShouldHandle = null!, + }; + + options + .Invoking(o => ValidationHelper.ValidateObject(o, "Dummy.")) + .Should() + .Throw() + .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. + """); + } +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerPredicateArgumentsTests.cs b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerPredicateArgumentsTests.cs new file mode 100644 index 00000000000..7baf0ba4d6c --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerPredicateArgumentsTests.cs @@ -0,0 +1,16 @@ +using Polly.CircuitBreaker; + +namespace Polly.Core.Tests.CircuitBreaker; + +public class CircuitBreakerPredicateArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var context = ResilienceContext.Get(); + + var args = new CircuitBreakerPredicateArguments(context); + + args.Context.Should().Be(context); + } +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs new file mode 100644 index 00000000000..df05852315d --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs @@ -0,0 +1,77 @@ +using System.ComponentModel.DataAnnotations; +using Polly.CircuitBreaker; +using Xunit; + +namespace Polly.Core.Tests.CircuitBreaker; + +public class CircuitBreakerResilienceStrategyBuilderTests +{ + public static TheoryData> ConfigureData = new() + { + builder => + { + builder.AddAdvancedCircuitBreaker(new AdvancedCircuitBreakerStrategyOptions()); + }, + builder => + { + builder.AddAdvancedCircuitBreaker(new AdvancedCircuitBreakerStrategyOptions()); + }, + builder => + { + builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions()); + }, + builder => + { + builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions()); + }, + }; + + [MemberData(nameof(ConfigureData))] + [Theory] + public void AddCircuitBreaker_Configure(Action builderAction) + { + var builder = new ResilienceStrategyBuilder(); + + builderAction(builder); + + var strategy = builder.Build(); + + strategy.Should().BeOfType(); + } + + [Fact] + public void AddCircuitBreaker_Validation() + { + var builder = new ResilienceStrategyBuilder(); + + builder + .Invoking(b => b.AddCircuitBreaker(new CircuitBreakerStrategyOptions { BreakDuration = TimeSpan.MinValue })) + .Should() + .Throw() + .WithMessage("The circuit breaker strategy options are invalid.*"); + + builder + .Invoking(b => b.AddCircuitBreaker(new CircuitBreakerStrategyOptions { BreakDuration = TimeSpan.MinValue })) + .Should() + .Throw() + .WithMessage("The circuit breaker strategy options are invalid.*"); + } + + [Fact] + public void AddAdvancedCircuitBreaker_Validation() + { + var builder = new ResilienceStrategyBuilder(); + + builder + .Invoking(b => b.AddAdvancedCircuitBreaker(new AdvancedCircuitBreakerStrategyOptions { BreakDuration = TimeSpan.MinValue })) + .Should() + .Throw() + .WithMessage("The advanced circuit breaker strategy options are invalid.*"); + + builder + .Invoking(b => b.AddAdvancedCircuitBreaker(new AdvancedCircuitBreakerStrategyOptions { BreakDuration = TimeSpan.MinValue })) + .Should() + .Throw() + .WithMessage("The advanced circuit breaker strategy options are invalid.*"); + } +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs new file mode 100644 index 00000000000..a95f72e4c63 --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs @@ -0,0 +1,31 @@ +using Moq; +using Polly.CircuitBreaker; +using Polly.Strategy; + +namespace Polly.Core.Tests.CircuitBreaker; + +public class CircuitBreakerResilienceStrategyTests +{ + private readonly FakeTimeProvider _timeProvider; + private readonly ResilienceStrategyTelemetry _telemetry; + + public CircuitBreakerResilienceStrategyTests() + { + _timeProvider = new FakeTimeProvider(); + _telemetry = TestUtilities.CreateResilienceTelemetry(Mock.Of()); + } + + [Fact] + public void Ctor_Ok() + { + Create().Should().NotBeNull(); + } + + [Fact] + public void Execute_Ok() + { + Create().Invoking(s => s.Execute(_ => { })).Should().NotThrow(); + } + + private CircuitBreakerResilienceStrategy Create() => new(_timeProvider.Object, _telemetry, new CircuitBreakerStrategyOptions()); +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerStateProviderTests.cs b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerStateProviderTests.cs new file mode 100644 index 00000000000..79fba82d76a --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/CircuitBreakerStateProviderTests.cs @@ -0,0 +1,73 @@ +using System; +using Polly.CircuitBreaker; + +namespace Polly.Core.Tests.CircuitBreaker; + +public class CircuitBreakerStateProviderTests +{ + [Fact] + public void Ctor_Ok() + { + var provider = new CircuitBreakerStateProvider(); + + provider.IsInitialized.Should().BeFalse(); + } + + [Fact] + public void NotInitialized_EnsureDefaults() + { + var provider = new CircuitBreakerStateProvider(); + + provider.CircuitState.Should().Be(CircuitState.Closed); + provider.LastException.Should().Be(null); + } + + [Fact] + public async Task ResetAsync_NotInitialized_Throws() + { + var control = new CircuitBreakerManualControl(); + + await control + .Invoking(c => c.ResetAsync(CancellationToken.None)) + .Should() + .ThrowAsync(); + } + + [Fact] + public void Initialize_Twice_Throws() + { + var provider = new CircuitBreakerStateProvider(); + provider.Initialize(() => CircuitState.Closed, () => null); + + provider + .Invoking(c => c.Initialize(() => CircuitState.Closed, () => null)) + .Should() + .Throw(); + } + + [Fact] + public void Initialize_Ok() + { + var provider = new CircuitBreakerStateProvider(); + var stateCalled = false; + var exceptionCalled = false; + + provider.Initialize( + () => + { + stateCalled = true; + return CircuitState.HalfOpen; + }, + () => + { + exceptionCalled = true; + return new InvalidOperationException(); + }); + + provider.CircuitState.Should().Be(CircuitState.HalfOpen); + provider.LastException.Should().BeOfType(); + + stateCalled.Should().BeTrue(); + exceptionCalled.Should().BeTrue(); + } +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/IsolatedCircuitExceptionTests.cs b/src/Polly.Core.Tests/CircuitBreaker/IsolatedCircuitExceptionTests.cs new file mode 100644 index 00000000000..d81cf6342b4 --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/IsolatedCircuitExceptionTests.cs @@ -0,0 +1,23 @@ +using Polly.CircuitBreaker; + +namespace Polly.Core.Tests.CircuitBreaker; + +public class IsolatedCircuitExceptionTests +{ + [Fact] + public void Ctor_Ok() + { + new IsolatedCircuitException("Dummy.").Message.Should().Be("Dummy."); + new IsolatedCircuitException().Message.Should().Be("The circuit is manually held open and is not allowing calls."); + new IsolatedCircuitException("Dummy.", new InvalidOperationException()).Message.Should().Be("Dummy."); + new IsolatedCircuitException("Dummy.", new InvalidOperationException()).InnerException.Should().BeOfType(); + } + +#if !NETCOREAPP + [Fact] + public void BinarySerialization_Ok() + { + BinarySerializationUtil.SerializeAndDeserializeException(new IsolatedCircuitException("dummy")).Should().NotBeNull(); + } +#endif +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/OnCircuitClosedArgumentsTests.cs b/src/Polly.Core.Tests/CircuitBreaker/OnCircuitClosedArgumentsTests.cs new file mode 100644 index 00000000000..316aec95dde --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/OnCircuitClosedArgumentsTests.cs @@ -0,0 +1,16 @@ +using Polly.CircuitBreaker; + +namespace Polly.Core.Tests.CircuitBreaker; + +public class OnCircuitClosedArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var context = ResilienceContext.Get(); + + var args = new OnCircuitClosedArguments(context); + + args.Context.Should().Be(context); + } +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/OnCircuitHalfOpenedArgumentsTests.cs b/src/Polly.Core.Tests/CircuitBreaker/OnCircuitHalfOpenedArgumentsTests.cs new file mode 100644 index 00000000000..f515f8e317d --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/OnCircuitHalfOpenedArgumentsTests.cs @@ -0,0 +1,16 @@ +using Polly.CircuitBreaker; + +namespace Polly.Core.Tests.CircuitBreaker; + +public class OnCircuitHalfOpenedArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var context = ResilienceContext.Get(); + + var args = new OnCircuitHalfOpenedArguments(context); + + args.Context.Should().Be(context); + } +} diff --git a/src/Polly.Core.Tests/CircuitBreaker/OnCircuitOpenedArgumentsTests.cs b/src/Polly.Core.Tests/CircuitBreaker/OnCircuitOpenedArgumentsTests.cs new file mode 100644 index 00000000000..639dd1acea2 --- /dev/null +++ b/src/Polly.Core.Tests/CircuitBreaker/OnCircuitOpenedArgumentsTests.cs @@ -0,0 +1,17 @@ +using Polly.CircuitBreaker; + +namespace Polly.Core.Tests.CircuitBreaker; + +public class OnCircuitOpenedArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var context = ResilienceContext.Get(); + + var args = new OnCircuitOpenedArguments(context, TimeSpan.FromSeconds(2)); + + args.Context.Should().Be(context); + args.BreakDuration.Should().Be(TimeSpan.FromSeconds(2)); + } +} diff --git a/src/Polly.Core/CircuitBreaker/AdvancedCircuitBreakerStrategyOptions.TResult.cs b/src/Polly.Core/CircuitBreaker/AdvancedCircuitBreakerStrategyOptions.TResult.cs new file mode 100644 index 00000000000..4a31c3eb4de --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/AdvancedCircuitBreakerStrategyOptions.TResult.cs @@ -0,0 +1,64 @@ +using System.ComponentModel.DataAnnotations; + +namespace Polly.CircuitBreaker; + +/// +/// The options for advanced circuit breaker resilience strategy. +/// The circuit will break if, within any time-slice of duration , +/// the proportion of actions resulting in a handled exception exceeds , +/// provided also that the number of actions through the circuit in the time-slice is at least . +/// The circuit will stay broken for the . +/// Any attempt to execute this while the circuit is broken, will immediately throw a containing the exception +/// that broke the circuit. +/// +/// If the first action after the break duration period results in a handled exception, the circuit will break +/// again for another ; if no exception is thrown, the circuit will reset. +/// +/// +/// The type of result the circuit breaker strategy handles. +public class AdvancedCircuitBreakerStrategyOptions : BaseCircuitBreakerStrategyOptions +{ + /// + /// Gets or sets the failure threshold at which the circuit will break. + /// + /// + /// A number between zero and one (inclusive) e.g. 0.5 represents breaking if 50% or more of actions result in a handled failure. + /// + /// A ratio number higher than 0, up to 1. + /// Defaults to 0.1 (i.e. 10%). + /// + /// + [Range(0, 1.0)] + public double FailureThreshold { get; set; } = CircuitBreakerConstants.DefaultAdvancedFailureThreshold; + + /// + /// Gets or sets the minimum throughput: this many actions or more must pass through the circuit in the time-slice, + /// for statistics to be considered significant and the circuit-breaker to come into action. + /// + /// + /// Value must be 2 or greater. + /// Defaults to 100. + /// + [Range(CircuitBreakerConstants.MinimumValidThroughput, int.MaxValue)] + public int MinimumThroughput { get; set; } = CircuitBreakerConstants.DefaultMinimumThroughput; + + /// + /// Gets or sets the duration of the sampling over which failure ratios are assessed. + /// + /// + /// Value must be greater than 0.5 seconds. Defaults to 30 seconds. + /// + [TimeSpan("00:00:00.500")] + public TimeSpan SamplingDuration { get; set; } = CircuitBreakerConstants.DefaultSamplingDuration; + + internal AdvancedCircuitBreakerStrategyOptions AsNonGenericOptions() + { + var options = new AdvancedCircuitBreakerStrategyOptions(); + UpdateNonGenericOptions(options); + options.FailureThreshold = FailureThreshold; + options.MinimumThroughput = MinimumThroughput; + options.SamplingDuration = SamplingDuration; + + return options; + } +} diff --git a/src/Polly.Core/CircuitBreaker/AdvancedCircuitBreakerStrategyOptions.cs b/src/Polly.Core/CircuitBreaker/AdvancedCircuitBreakerStrategyOptions.cs new file mode 100644 index 00000000000..3714a7ead00 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/AdvancedCircuitBreakerStrategyOptions.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; + +namespace Polly.CircuitBreaker; + +/// +/// The options for advanced circuit breaker resilience strategy. +/// The circuit will break if, within any time-slice of duration , +/// the proportion of actions resulting in a handled exception exceeds , +/// provided also that the number of actions through the circuit in the time-slice is at least . +/// The circuit will stay broken for the . +/// Any attempt to execute this while the circuit is broken, will immediately throw a containing the exception +/// that broke the circuit. +/// +/// If the first action after the break duration period results in a handled exception, the circuit will break +/// again for another ; if no exception is thrown, the circuit will reset. +/// +/// +public class AdvancedCircuitBreakerStrategyOptions : BaseCircuitBreakerStrategyOptions +{ + /// + /// Gets or sets the failure threshold at which the circuit will break. + /// + /// + /// A number between zero and one (inclusive) e.g. 0.5 represents breaking if 50% or more of actions result in a handled failure. + /// + /// A ratio number higher than 0, up to 1. + /// Defaults to 0.1 (i.e. 10%). + /// + /// + [Range(0, 1.0)] + public double FailureThreshold { get; set; } = CircuitBreakerConstants.DefaultAdvancedFailureThreshold; + + /// + /// Gets or sets the minimum throughput: this many actions or more must pass through the circuit in the time-slice, + /// for statistics to be considered significant and the circuit-breaker to come into action. + /// + /// + /// Value must be 2 or greater. + /// Defaults to 100. + /// + [Range(CircuitBreakerConstants.MinimumValidThroughput, int.MaxValue)] + public int MinimumThroughput { get; set; } = CircuitBreakerConstants.DefaultMinimumThroughput; + + /// + /// Gets or sets the duration of the sampling over which failure ratios are assessed. + /// + /// + /// Value must be greater than 0.5 seconds. Defaults to 30 seconds. + /// + [TimeSpan("00:00:00.500")] + public TimeSpan SamplingDuration { get; set; } = CircuitBreakerConstants.DefaultSamplingDuration; +} diff --git a/src/Polly.Core/CircuitBreaker/BaseCircuitBreakerStrategyOptions.TResult.cs b/src/Polly.Core/CircuitBreaker/BaseCircuitBreakerStrategyOptions.TResult.cs new file mode 100644 index 00000000000..a12e375b6d6 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/BaseCircuitBreakerStrategyOptions.TResult.cs @@ -0,0 +1,87 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.CircuitBreaker; + +/// +/// The base options for circuit breaker resilience strategy. +/// The circuit will stay broken for the . Any attempt to execute the resilience strategy +/// while the circuit is broken, will immediately throw a containing the exception or result +/// that broke the circuit. +/// +/// If the first action after the break duration period results in a handled exception or result, the circuit will break +/// again for another ; if no exception or handled result is encountered, the circuit will reset. +/// +/// +/// The type of result the circuit breaker strategy handles. +public abstract class BaseCircuitBreakerStrategyOptions : ResilienceStrategyOptions +{ + /// + /// Initializes a new instance of the class. + /// + protected BaseCircuitBreakerStrategyOptions() => StrategyType = CircuitBreakerConstants.StrategyType; + + /// + /// Gets or sets the duration of break the circuit will stay open before resetting. + /// + /// + /// Value must be greater than 0.5 seconds. + /// Defaults to 5 seconds. + /// + [TimeSpan("00:00:00.500")] + public TimeSpan BreakDuration { get; set; } = CircuitBreakerConstants.DefaultBreakDuration; + + /// + /// Gets or sets the predicates for the circuit breaker. + /// + [Required] + public OutcomePredicate ShouldHandle { get; set; } = new(); + + /// + /// Gets or sets the event that is raised when the circuit resets to a state. + /// + [Required] + public OutcomeEvent OnClosed { get; set; } = new(); + + /// + /// Gets or sets the event that is raised when the circuit transitions to an state. + /// + [Required] + public OutcomeEvent OnOpened { get; set; } = new(); + + /// + /// Gets or sets the event that is raised when when the circuit transitions to an state. + /// + [Required] + public NoOutcomeEvent OnHalfOpened { get; set; } = new(); + + /// + /// Gets or sets the manual control for the circuit breaker. + /// + /// + /// Defaults to null. + /// + public CircuitBreakerManualControl? ManualControl { get; set; } + + /// + /// Gets or sets the state provider for the circuit breaker. + /// + /// + /// Defaults to null. + /// + public CircuitBreakerStateProvider? StateProvider { get; set; } + + internal void UpdateNonGenericOptions(BaseCircuitBreakerStrategyOptions options) + { + options.BreakDuration = BreakDuration; + options.StrategyName = StrategyName; + options.StrategyType = StrategyType; + options.ShouldHandle = new OutcomePredicate().SetPredicates(ShouldHandle); + options.OnClosed = new OutcomeEvent().SetCallbacks(OnClosed); + options.OnOpened = new OutcomeEvent().SetCallbacks(OnOpened); + options.OnHalfOpened = OnHalfOpened; + options.ManualControl = ManualControl; + options.StateProvider = StateProvider; + } +} + diff --git a/src/Polly.Core/CircuitBreaker/BaseCircuitBreakerStrategyOptions.cs b/src/Polly.Core/CircuitBreaker/BaseCircuitBreakerStrategyOptions.cs new file mode 100644 index 00000000000..130ba1f007a --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/BaseCircuitBreakerStrategyOptions.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.CircuitBreaker; + +/// +/// The base options for circuit breaker resilience strategy. +/// The circuit will stay broken for the . Any attempt to execute the resilience strategy +/// while the circuit is broken, will immediately throw a containing the exception or result +/// that broke the circuit. +/// +/// If the first action after the break duration period results in a handled exception or result, the circuit will break +/// again for another ; if no exception or handled result is encountered, the circuit will reset. +/// +/// +public abstract class BaseCircuitBreakerStrategyOptions : ResilienceStrategyOptions +{ + /// + /// Initializes a new instance of the class. + /// + protected BaseCircuitBreakerStrategyOptions() => StrategyType = CircuitBreakerConstants.StrategyType; + + /// + /// Gets or sets the duration of break the circuit will stay open before resetting. + /// + /// + /// Value must be greater than 0.5 seconds. + /// Defaults to 5 seconds. + /// + [TimeSpan("00:00:00.500")] + public TimeSpan BreakDuration { get; set; } = CircuitBreakerConstants.DefaultBreakDuration; + + /// + /// Gets or sets the predicates for the circuit breaker. + /// + [Required] + public OutcomePredicate ShouldHandle { get; set; } = new(); + + /// + /// Gets or sets the event that is raised when the circuit resets to a state. + /// + [Required] + public OutcomeEvent OnClosed { get; set; } = new(); + + /// + /// Gets or sets the event that is raised when the circuit transitions to an state. + /// + [Required] + public OutcomeEvent OnOpened { get; set; } = new(); + + /// + /// Gets or sets the event that is raised when when the circuit transitions to an state. + /// + [Required] + public NoOutcomeEvent OnHalfOpened { get; set; } = new(); + + /// + /// Gets or sets the manual control for the circuit breaker. + /// + /// + /// Defaults to null. + /// + public CircuitBreakerManualControl? ManualControl { get; set; } + + /// + /// Gets or sets the state provider for the circuit breaker. + /// + /// + /// Defaults to null. + /// + public CircuitBreakerStateProvider? StateProvider { get; set; } + +} diff --git a/src/Polly.Core/CircuitBreaker/BrokenCircuitException.TResult.cs b/src/Polly.Core/CircuitBreaker/BrokenCircuitException.TResult.cs new file mode 100644 index 00000000000..23955e4bdc5 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/BrokenCircuitException.TResult.cs @@ -0,0 +1,55 @@ +#if !NETCOREAPP +using System.Runtime.Serialization; +#endif + +namespace Polly.CircuitBreaker; + +#pragma warning disable CA1032 // Implement standard exception constructors + +/// +/// Exception thrown when a circuit is broken. +/// +/// The type of returned results being handled by the policy. +#if !NETCOREAPP +[Serializable] +#endif +public class BrokenCircuitException : BrokenCircuitException +{ + /// + /// Initializes a new instance of the class. + /// + /// The result which caused the circuit to break. + public BrokenCircuitException(TResult result) => Result = result; + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The result which caused the circuit to break. + public BrokenCircuitException(string message, TResult result) + : base(message) => Result = result; + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The inner exception. + /// The result which caused the circuit to break. + public BrokenCircuitException(string message, Exception inner, TResult result) + : base(message, inner) => Result = result; + + /// + /// Gets the result value which was considered a handled fault, by the policy. + /// + public TResult Result { get; } + +#if !NETCOREAPP + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected BrokenCircuitException(SerializationInfo info, StreamingContext context) + : base(info, context) => Result = default!; +#endif +} diff --git a/src/Polly.Core/CircuitBreaker/BrokenCircuitException.cs b/src/Polly.Core/CircuitBreaker/BrokenCircuitException.cs new file mode 100644 index 00000000000..357a0d97a9e --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/BrokenCircuitException.cs @@ -0,0 +1,52 @@ +#if !NETCOREAPP +using System.Runtime.Serialization; +#endif + +namespace Polly.CircuitBreaker; + +/// +/// Exception thrown when a circuit is broken. +/// +#if !NETCOREAPP +[Serializable] +#endif +public class BrokenCircuitException : ExecutionRejectedException +{ + /// + /// Initializes a new instance of the class. + /// + public BrokenCircuitException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public BrokenCircuitException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The inner exception. + public BrokenCircuitException(string message, Exception inner) + : base(message, inner) + { + } + +#if !NETCOREAPP + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected BrokenCircuitException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +#endif +} diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerConstants.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerConstants.cs new file mode 100644 index 00000000000..3a5835c2be8 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerConstants.cs @@ -0,0 +1,24 @@ +namespace Polly.CircuitBreaker; + +internal static class CircuitBreakerConstants +{ + public const string StrategyType = "CircuitBreaker"; + + public const string OnResetEvent = "OnCircuitReset"; + + public const string OnHalfOpenEvent = "OnCircuitHalfOpen"; + + public const string OnBreakEvent = "OnCircuitBreak"; + + public const double DefaultAdvancedFailureThreshold = 0.1; + + public const int DefaultMinimumThroughput = 100; + + public const int MinimumValidThroughput = 2; + + public const int DefaultFailureThreshold = 100; + + public static readonly TimeSpan DefaultBreakDuration = TimeSpan.FromSeconds(5); + + public static readonly TimeSpan DefaultSamplingDuration = TimeSpan.FromSeconds(30); +} diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerManualControl.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerManualControl.cs new file mode 100644 index 00000000000..498a1797e9c --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerManualControl.cs @@ -0,0 +1,112 @@ +using System; + +namespace Polly.CircuitBreaker; + +/// +/// Allows manual control of the circuit-breaker. +/// +public sealed class CircuitBreakerManualControl +{ + private Func? _onIsolate; + private Func? _onReset; + + internal void Initialize(Func onIsolate, Func onReset) + { + if (_onIsolate != null) + { + throw new InvalidOperationException($"This instance of '{nameof(CircuitBreakerManualControl)}' is already initialized and cannot be used in a different circuit-breaker strategy."); + } + + _onIsolate = onIsolate; + _onReset = onReset; + } + + /// + /// Gets a value indicating whether the manual control is initialized. + /// + /// + /// The initialization happens when the circuit-breaker strategy is attached to this class. + /// This happens when the final strategy is created by the call. + /// + public bool IsInitialized => _onIsolate != null; + + /// + /// Isolates (opens) the circuit manually, and holds it in this state until a call to is made. + /// + /// The resilience context. + /// The instance of that represents the asynchronous execution. + /// Thrown if manual control is not initialized. + public Task IsolateAsync(ResilienceContext context) + { + Guard.NotNull(context); + + if (_onIsolate == null) + { + throw new InvalidOperationException("The circuit-breaker manual control is not initialized"); + } + + context.Initialize(isSynchronous: false); + return _onIsolate(context); + } + + /// + /// Isolates (opens) the circuit manually, and holds it in this state until a call to is made. + /// + /// The cancellation token. + /// The instance of that represents the asynchronous execution. + /// Thrown if manual control is not initialized. + public async Task IsolateAsync(CancellationToken cancellationToken) + { + var context = ResilienceContext.Get(); + context.CancellationToken = cancellationToken; + + try + { + await IsolateAsync(context).ConfigureAwait(false); + } + finally + { + ResilienceContext.Return(context); + } + } + + /// + /// Closes the circuit, and resets any statistics controlling automated circuit-breaking. + /// + /// The resilience context. + /// The instance of that represents the asynchronous execution. + /// Thrown if manual control is not initialized. + public Task ResetAsync(ResilienceContext context) + { + Guard.NotNull(context); + + if (_onReset == null) + { + throw new InvalidOperationException("The circuit-breaker manual control is not initialized"); + } + + context.Initialize(isSynchronous: false); + return _onReset(context); + } + + /// + /// Closes the circuit, and resets any statistics controlling automated circuit-breaking. + /// + /// The cancellation token. + /// The instance of that represents the asynchronous execution. + /// Thrown if manual control is not initialized. + public async Task ResetAsync(CancellationToken cancellationToken) + { + var context = ResilienceContext.Get(); + context.CancellationToken = cancellationToken; + + try + { + await ResetAsync(context).ConfigureAwait(false); + } + finally + { + ResilienceContext.Return(context); + } + } +} diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerPredicateArguments.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerPredicateArguments.cs new file mode 100644 index 00000000000..35d054529be --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerPredicateArguments.cs @@ -0,0 +1,16 @@ +using Polly.Strategy; + +namespace Polly.CircuitBreaker; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by predicate. +/// +public readonly struct CircuitBreakerPredicateArguments : IResilienceArguments +{ + internal CircuitBreakerPredicateArguments(ResilienceContext context) => Context = context; + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs new file mode 100644 index 00000000000..6f702d0464b --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs @@ -0,0 +1,25 @@ +using Polly.Strategy; + +namespace Polly.CircuitBreaker; + +internal sealed class CircuitBreakerResilienceStrategy : ResilienceStrategy +{ +#pragma warning disable IDE0052 // Remove unread private members + private readonly TimeProvider _timeProvider; + private readonly ResilienceStrategyTelemetry _telemetry; + private readonly BaseCircuitBreakerStrategyOptions _options; +#pragma warning restore IDE0052 // Remove unread private members + + public CircuitBreakerResilienceStrategy(TimeProvider timeProvider, ResilienceStrategyTelemetry telemetry, BaseCircuitBreakerStrategyOptions options) + { + _timeProvider = timeProvider; + _telemetry = telemetry; + _options = options; + } + + protected internal override ValueTask ExecuteCoreAsync(Func> callback, ResilienceContext context, TState state) + { + return callback(context, state); + } +} + diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderExtensions.cs new file mode 100644 index 00000000000..67ab92afa5f --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderExtensions.cs @@ -0,0 +1,94 @@ +using Polly.CircuitBreaker; +using Polly.Strategy; + +namespace Polly; + +/// +/// Circuit breaker strategy extensions for . +/// +public static class CircuitBreakerResilienceStrategyBuilderExtensions +{ + /// + /// Add advanced circuit breaker strategy to the builder. + /// + /// The type of result the circuit breaker strategy handles. + /// The builder instance. + /// The options instance. + /// A builder with the circuit breaker strategy added. + /// + /// See for more details about the advanced circuit breaker strategy. + /// + public static ResilienceStrategyBuilder AddAdvancedCircuitBreaker(this ResilienceStrategyBuilder builder, AdvancedCircuitBreakerStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The advanced circuit breaker strategy options are invalid."); + + return builder.AddCircuitBreakerCore(options.AsNonGenericOptions()); + } + + /// + /// Add advanced circuit breaker strategy to the builder. + /// + /// The builder instance. + /// The options instance. + /// A builder with the circuit breaker strategy added. + /// + /// See for more details about the advanced circuit breaker strategy. + /// + public static ResilienceStrategyBuilder AddAdvancedCircuitBreaker(this ResilienceStrategyBuilder builder, AdvancedCircuitBreakerStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The advanced circuit breaker strategy options are invalid."); + + return builder.AddCircuitBreakerCore(options); + } + + /// + /// Add simple circuit breaker strategy to the builder. + /// + /// The type of result the circuit breaker strategy handles. + /// The builder instance. + /// The options instance. + /// A builder with the circuit breaker strategy added. + /// + /// See for more details about the advanced circuit breaker strategy. + /// + public static ResilienceStrategyBuilder AddCircuitBreaker(this ResilienceStrategyBuilder builder, CircuitBreakerStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The circuit breaker strategy options are invalid."); + + return builder.AddCircuitBreakerCore(options.AsNonGenericOptions()); + } + + /// + /// Add simple circuit breaker strategy to the builder. + /// + /// The builder instance. + /// The options instance. + /// A builder with the circuit breaker strategy added. + /// + /// See for more details about the advanced circuit breaker strategy. + /// + public static ResilienceStrategyBuilder AddCircuitBreaker(this ResilienceStrategyBuilder builder, CircuitBreakerStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The circuit breaker strategy options are invalid."); + + return builder.AddCircuitBreakerCore(options); + } + + private static ResilienceStrategyBuilder AddCircuitBreakerCore(this ResilienceStrategyBuilder builder, BaseCircuitBreakerStrategyOptions options) + { + return builder.AddStrategy(context => new CircuitBreakerResilienceStrategy(context.TimeProvider, context.Telemetry, options)); + } +} + diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerStateProvider.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerStateProvider.cs new file mode 100644 index 00000000000..13c87fcd2d7 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerStateProvider.cs @@ -0,0 +1,41 @@ +namespace Polly.CircuitBreaker; + +/// +/// Allows retrieval of the circuit breaker state. +/// +public sealed class CircuitBreakerStateProvider +{ + private Func? _circuitStateProvider; + private Func? _lastExceptionProvider; + + internal void Initialize(Func circuitStateProvider, Func lastExceptionProvider) + { + if (_circuitStateProvider != null) + { + throw new InvalidOperationException($"This instance of '{nameof(CircuitBreakerStateProvider)}' is already initialized and cannot be used in a different circuit-breaker strategy."); + } + + _circuitStateProvider = circuitStateProvider; + _lastExceptionProvider = lastExceptionProvider; + } + + /// + /// Gets a value indicating whether the state provider is initialized. + /// + /// + /// The initialization happens when the circuit-breaker strategy is attached to this class. + /// This happens when the final strategy is created by the call. + /// + public bool IsInitialized => _circuitStateProvider != null; + + /// + /// Gets the state of the underlying circuit. + /// + public CircuitState CircuitState => _circuitStateProvider?.Invoke() ?? CircuitState.Closed; + + /// + /// Gets the last exception handled by the circuit-breaker. + /// This will be null if no exceptions have been handled by the circuit-breaker since the circuit last closed. + /// + public Exception? LastException => _lastExceptionProvider?.Invoke(); +} diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.TResult.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.TResult.cs new file mode 100644 index 00000000000..03aefc206b4 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.TResult.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; + +namespace Polly.CircuitBreaker; + +/// +/// The options for circuit breaker resilience strategy. +/// The circuit will break if +/// exceptions or results that are handled by the resilience strategy are encountered consecutively. +/// The circuit will stay broken for the . Any attempt to execute the resilience strategy +/// while the circuit is broken, will immediately throw a containing the exception or result +/// that broke the circuit. +/// +/// If the first action after the break duration period results in a handled exception or result, the circuit will break +/// again for another ; if no exception or handled result is encountered, the circuit will reset. +/// +/// +/// The type of result the circuit breaker strategy handles. +public class CircuitBreakerStrategyOptions : BaseCircuitBreakerStrategyOptions +{ + /// + /// Gets or sets the number of the outcome failures handled by before opening the circuit. + /// + /// + /// Must be greater than 0. Defaults to 100. + /// + [Range(1, int.MaxValue)] + public int FailureThreshold { get; set; } = CircuitBreakerConstants.DefaultFailureThreshold; + + internal CircuitBreakerStrategyOptions AsNonGenericOptions() + { + var options = new CircuitBreakerStrategyOptions(); + UpdateNonGenericOptions(options); + options.FailureThreshold = FailureThreshold; + + return options; + } +} diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs new file mode 100644 index 00000000000..26c0b45e77b --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace Polly.CircuitBreaker; + +/// +/// The options for circuit breaker resilience strategy. +/// The circuit will break if +/// exceptions or results that are handled by the resilience strategy are encountered consecutively. +/// The circuit will stay broken for the . Any attempt to execute the resilience strategy +/// while the circuit is broken, will immediately throw a containing the exception or result +/// that broke the circuit. +/// +/// If the first action after the break duration period results in a handled exception or result, the circuit will break +/// again for another ; if no exception or handled result is encountered, the circuit will reset. +/// +/// +public class CircuitBreakerStrategyOptions : BaseCircuitBreakerStrategyOptions +{ + /// + /// Gets or sets the number of the outcome failures handled by before opening the circuit. + /// + /// + /// Defaults to 100. + /// + [Range(1, int.MaxValue)] + public int FailureThreshold { get; set; } = CircuitBreakerConstants.DefaultFailureThreshold; +} diff --git a/src/Polly.Core/CircuitBreaker/CircuitState.cs b/src/Polly.Core/CircuitBreaker/CircuitState.cs new file mode 100644 index 00000000000..a027f2c5350 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/CircuitState.cs @@ -0,0 +1,32 @@ +namespace Polly.CircuitBreaker; + +/// +/// Describes the possible states the circuit of a Circuit Breaker may be in. +/// +public enum CircuitState +{ + /// + /// Closed - When the circuit is closed. Execution of actions is allowed. + /// + Closed, + + /// + /// Open - When the automated controller has opened the circuit (typically due to some failure threshold being exceeded by recent actions). Execution of actions is blocked. + /// + Open, + + /// + /// Half-open - When the circuit is half-open, it is recovering from an open state. + /// The duration of break of the preceding open state has typically passed. + /// In the half-open state, actions may be executed, but the results of these actions may be treated with criteria different to normal operation, + /// to decide if the circuit has recovered sufficiently to be placed back in to the closed state, + /// or if continuing failures mean the circuit should revert to open perhaps more quickly than in normal operation. + /// + HalfOpen, + + /// + /// Isolated - When the circuit has been placed into a fixed open state by the isolate call. + /// This isolates the circuit manually, blocking execution of all actions until a reset call is made. + /// + Isolated +} diff --git a/src/Polly.Core/CircuitBreaker/IsolatedCircuitException.cs b/src/Polly.Core/CircuitBreaker/IsolatedCircuitException.cs new file mode 100644 index 00000000000..07c9054db8a --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/IsolatedCircuitException.cs @@ -0,0 +1,53 @@ +#if !NETCOREAPP +using System.Runtime.Serialization; +#endif + +namespace Polly.CircuitBreaker; + +/// +/// Exception thrown when a circuit is isolated (held open) by manual override. +/// +#if !NETCOREAPP +[Serializable] +#endif +public class IsolatedCircuitException : BrokenCircuitException +{ + /// + /// Initializes a new instance of the class. + /// + public IsolatedCircuitException() + : base("The circuit is manually held open and is not allowing calls.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public IsolatedCircuitException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The inner exception. + public IsolatedCircuitException(string message, Exception innerException) + : base(message, innerException) + { + } + +#if !NETCOREAPP + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected IsolatedCircuitException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +#endif +} diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs new file mode 100644 index 00000000000..3e8a7f11dee --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs @@ -0,0 +1,16 @@ +using Polly.Strategy; + +namespace Polly.CircuitBreaker; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by event. +/// +public readonly struct OnCircuitClosedArguments : IResilienceArguments +{ + internal OnCircuitClosedArguments(ResilienceContext context) => Context = context; + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs new file mode 100644 index 00000000000..ff26206e492 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs @@ -0,0 +1,16 @@ +using Polly.Strategy; + +namespace Polly.CircuitBreaker; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by event. +/// +public readonly struct OnCircuitHalfOpenedArguments : IResilienceArguments +{ + internal OnCircuitHalfOpenedArguments(ResilienceContext context) => Context = context; + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs new file mode 100644 index 00000000000..dbfaf80d5d0 --- /dev/null +++ b/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs @@ -0,0 +1,26 @@ +using Polly.Strategy; + +namespace Polly.CircuitBreaker; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by event. +/// +public readonly struct OnCircuitOpenedArguments : IResilienceArguments +{ + internal OnCircuitOpenedArguments(ResilienceContext context, TimeSpan breakDuration) + { + BreakDuration = breakDuration; + Context = context; + } + + /// + /// Gets the duration of break. + /// + public TimeSpan BreakDuration { get; } + + /// + public ResilienceContext Context { get; } +} + diff --git a/src/Polly.Core/Strategy/NoOutcomeEvent.cs b/src/Polly.Core/Strategy/NoOutcomeEvent.cs index 50e3f500591..be523bf0d2c 100644 --- a/src/Polly.Core/Strategy/NoOutcomeEvent.cs +++ b/src/Polly.Core/Strategy/NoOutcomeEvent.cs @@ -12,6 +12,11 @@ public sealed class NoOutcomeEvent { private readonly List> _callbacks = new(); + /// + /// Gets a value indicating whether the event is empty. + /// + public bool IsEmpty => _callbacks.Count == 0; + /// /// Adds an asynchronous event callback. /// diff --git a/src/Polly/CircuitBreaker/BrokenCircuitException.cs b/src/Polly/CircuitBreaker/BrokenCircuitException.cs deleted file mode 100644 index 700531cd7c3..00000000000 --- a/src/Polly/CircuitBreaker/BrokenCircuitException.cs +++ /dev/null @@ -1,96 +0,0 @@ -#if NETSTANDARD2_0 -using System.Runtime.Serialization; -#endif - -namespace Polly.CircuitBreaker; - -/// -/// Exception thrown when a circuit is broken. -/// -#if NETSTANDARD2_0 -[Serializable] -#endif -public class BrokenCircuitException : ExecutionRejectedException -{ - /// - /// Initializes a new instance of the class. - /// - public BrokenCircuitException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public BrokenCircuitException(string message) : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - /// The inner exception. - public BrokenCircuitException(string message, Exception inner) : base(message, inner) - { - } - -#if NETSTANDARD2_0 - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected BrokenCircuitException( - SerializationInfo info, - StreamingContext context) : base(info, context) - { - } -#endif -} - -/// -/// Exception thrown when a circuit is broken. -/// -/// The type of returned results being handled by the policy. -#if NETSTANDARD2_0 -[Serializable] -#endif -public class BrokenCircuitException : BrokenCircuitException -{ - private readonly TResult result; - - /// - /// Initializes a new instance of the class. - /// - /// The result which caused the circuit to break. - public BrokenCircuitException(TResult result) : base() => - this.result = result; - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - /// The result which caused the circuit to break. - public BrokenCircuitException(string message, TResult result) : base(message) => - this.result = result; - - /// - /// The result value which was considered a handled fault, by the policy. - /// - public TResult Result => result; - -#if NETSTANDARD2_0 - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected BrokenCircuitException( - SerializationInfo info, - StreamingContext context) : base(info, context) - { - } -#endif -} diff --git a/src/Polly/CircuitBreaker/CircuitState.cs b/src/Polly/CircuitBreaker/CircuitState.cs deleted file mode 100644 index 177644b90b7..00000000000 --- a/src/Polly/CircuitBreaker/CircuitState.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Polly.CircuitBreaker; - -/// -/// Describes the possible states the circuit of a CircuitBreaker may be in. -/// -public enum CircuitState -{ - /// - /// Closed - When the circuit is closed. Execution of actions is allowed. - /// - Closed, - /// - /// Open - When the automated controller has opened the circuit (typically due to some failure threshold being exceeded by recent actions). Execution of actions is blocked. - /// - Open, - /// - /// Half-open - When the circuit is half-open, it is recovering from an open state. The duration of break of the preceding open state has typically passed. In the half-open state, actions may be executed, but the results of these actions may be treated with criteria different to normal operation, to decide if the circuit has recovered sufficiently to be placed back in to the closed state, or if continuing failures mean the circuit should revert to open perhaps more quickly than in normal operation. - /// - HalfOpen, - /// - /// Isolated - When the circuit has been placed into a fixed open state by a call to . This isolates the circuit manually, blocking execution of all actions until a call to is made. - /// - Isolated -} diff --git a/src/Polly/CircuitBreaker/IsolatedCircuitException.cs b/src/Polly/CircuitBreaker/IsolatedCircuitException.cs deleted file mode 100644 index 124bcf63b5f..00000000000 --- a/src/Polly/CircuitBreaker/IsolatedCircuitException.cs +++ /dev/null @@ -1,33 +0,0 @@ -#if NETSTANDARD2_0 -using System.Runtime.Serialization; -#endif - -namespace Polly.CircuitBreaker; - -/// -/// Exception thrown when a circuit is isolated (held open) by manual override. -/// -#if NETSTANDARD2_0 -[Serializable] -#endif -public class IsolatedCircuitException : BrokenCircuitException -{ - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public IsolatedCircuitException(string message) : base(message) { } - -#if NETSTANDARD2_0 - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected IsolatedCircuitException( - SerializationInfo info, - StreamingContext context) : base(info, context) - { - } -#endif -} diff --git a/src/Polly/Properties/AssemblyInfo.cs b/src/Polly/Properties/AssemblyInfo.cs index c56e34f310b..80b49eed2b3 100644 --- a/src/Polly/Properties/AssemblyInfo.cs +++ b/src/Polly/Properties/AssemblyInfo.cs @@ -3,3 +3,7 @@ [assembly: TypeForwardedTo(typeof(ExecutionRejectedException))] [assembly: TypeForwardedTo(typeof(TimeoutRejectedException))] +[assembly: TypeForwardedTo(typeof(BrokenCircuitException))] +[assembly: TypeForwardedTo(typeof(BrokenCircuitException<>))] +[assembly: TypeForwardedTo(typeof(IsolatedCircuitException))] +[assembly: TypeForwardedTo(typeof(CircuitState))]