Skip to content

Commit

Permalink
Migrate CircuitStateController and implement `CircutBreakerResilien…
Browse files Browse the repository at this point in the history
…ceStrategy ` (#1152)
  • Loading branch information
martintmk committed Apr 23, 2023
1 parent de228b4 commit c7c8dbd
Show file tree
Hide file tree
Showing 30 changed files with 1,537 additions and 56 deletions.
1 change: 1 addition & 0 deletions build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Task("__RunTests")
Configuration = configuration,
Loggers = loggers,
NoBuild = true,
ArgumentCustomization = args => args.Append($"--blame-hang-timeout 10s")
});
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.0-preview-20230223-05" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="ReportGenerator" Version="5.1.19" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ public async Task AsNonGenericOptions_Ok()

(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));
await converted.OnClosed.CreateHandler()!.HandleAsync(new Outcome<int>(new InvalidOperationException()), new OnCircuitClosedArguments(context, true));
onResetCalled.Should().BeTrue();

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

await converted.OnHalfOpened.CreateHandler()!(new OnCircuitHalfOpenedArguments(context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ public class CircuitBreakerManualControlTests
[Fact]
public void Ctor_Ok()
{
var control = new CircuitBreakerManualControl();
using var control = new CircuitBreakerManualControl();

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

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

await control
.Invoking(c => c.IsolateAsync(CancellationToken.None))
Expand All @@ -27,22 +27,22 @@ await control
[Fact]
public async Task ResetAsync_NotInitialized_Throws()
{
var control = new CircuitBreakerManualControl();
using var control = new CircuitBreakerManualControl();

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

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

control
.Invoking(c => c.Initialize(_ => Task.CompletedTask, _ => Task.CompletedTask))
.Invoking(c => c.Initialize(_ => Task.CompletedTask, _ => Task.CompletedTask, () => { }))
.Should()
.Throw<InvalidOperationException>();
}
Expand All @@ -53,6 +53,7 @@ public async Task Initialize_Ok()
var control = new CircuitBreakerManualControl();
var isolateCalled = false;
var resetCalled = false;
var disposeCalled = false;

control.Initialize(
context =>
Expand All @@ -68,12 +69,16 @@ public async Task Initialize_Ok()
context.IsSynchronous.Should().BeFalse();
resetCalled = true;
return Task.CompletedTask;
});
},
() => disposeCalled = true);

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

control.Dispose();

isolateCalled.Should().BeTrue();
resetCalled.Should().BeTrue();
disposeCalled.Should().BeTrue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ public async Task AsNonGenericOptions_Ok()

(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));
await converted.OnClosed.CreateHandler()!.HandleAsync(new Outcome<int>(new InvalidOperationException()), new OnCircuitClosedArguments(context, true));
onResetCalled.Should().BeTrue();

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

await converted.OnHalfOpened.CreateHandler()!(new OnCircuitHalfOpenedArguments(context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,87 @@ public void AddAdvancedCircuitBreaker_Validation()
.Throw<ValidationException>()
.WithMessage("The advanced circuit breaker strategy options are invalid.*");
}

[Fact]
public void AddCircuitBreaker_IntegrationTest()
{
int opened = 0;
int closed = 0;
int halfOpened = 0;

var options = new CircuitBreakerStrategyOptions
{
FailureThreshold = 5,
BreakDuration = TimeSpan.FromMilliseconds(500),
};

options.ShouldHandle.HandleResult(-1);
options.OnOpened.Register<int>(() => opened++);
options.OnClosed.Register<int>(() => closed++);
options.OnHalfOpened.Register(() => halfOpened++);

var timeProvider = new FakeTimeProvider();
var strategy = new ResilienceStrategyBuilder { TimeProvider = timeProvider.Object }.AddCircuitBreaker(options).Build();
var time = DateTime.UtcNow;
timeProvider.Setup(v => v.UtcNow).Returns(() => time);

for (int i = 0; i < options.FailureThreshold; i++)
{
strategy.Execute(_ => -1);
}

// Circuit opened
opened.Should().Be(1);
halfOpened.Should().Be(0);
closed.Should().Be(0);
Assert.Throws<BrokenCircuitException<int>>(() => strategy.Execute(_ => 0));

// Circuit Half Opened
time += options.BreakDuration;
strategy.Execute(_ => -1);
Assert.Throws<BrokenCircuitException<int>>(() => strategy.Execute(_ => 0));
opened.Should().Be(2);
halfOpened.Should().Be(1);
closed.Should().Be(0);

// Now close it
time += options.BreakDuration;
strategy.Execute(_ => 0);
opened.Should().Be(2);
halfOpened.Should().Be(2);
closed.Should().Be(1);
}

[Fact]
public void AddAdvancedCircuitBreaker_IntegrationTest()
{
var options = new AdvancedCircuitBreakerStrategyOptions
{
BreakDuration = TimeSpan.FromMilliseconds(500),
};

options.ShouldHandle.HandleResult(-1);
options.OnOpened.Register<int>(() => { });
options.OnClosed.Register<int>(() => { });
options.OnHalfOpened.Register(() => { });

var timeProvider = new FakeTimeProvider();
var strategy = new ResilienceStrategyBuilder { TimeProvider = timeProvider.Object }.AddAdvancedCircuitBreaker(options).Build();
var time = DateTime.UtcNow;
timeProvider.Setup(v => v.UtcNow).Returns(() => time);

strategy.Should().BeOfType<CircuitBreakerResilienceStrategy>();
}

[Fact]
public void AddCircuitBreaker_UnrecognizedOptions_Throws()
{
var builder = new ResilienceStrategyBuilder();

builder.Invoking(b => b.AddCircuitBreakerCore(new DummyOptions()).Build()).Should().Throw<NotSupportedException>();
}

private class DummyOptions : BaseCircuitBreakerStrategyOptions
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,127 @@

namespace Polly.Core.Tests.CircuitBreaker;

public class CircuitBreakerResilienceStrategyTests
public class CircuitBreakerResilienceStrategyTests : IDisposable
{
private readonly FakeTimeProvider _timeProvider;
private readonly Mock<CircuitBehavior> _behavior;
private readonly ResilienceStrategyTelemetry _telemetry;
private readonly CircuitBreakerStrategyOptions _options;
private readonly CircuitStateController _controller;

public CircuitBreakerResilienceStrategyTests()
{
_timeProvider = new FakeTimeProvider();
_timeProvider.Setup(v => v.UtcNow).Returns(DateTime.UtcNow);
_behavior = new Mock<CircuitBehavior>(MockBehavior.Strict);
_telemetry = TestUtilities.CreateResilienceTelemetry(Mock.Of<DiagnosticSource>());
_options = new CircuitBreakerStrategyOptions();
_controller = new CircuitStateController(
new CircuitBreakerStrategyOptions(),
_behavior.Object,
_timeProvider.Object,
_telemetry);
}

[Fact]
public void Ctor_Ok()
{
Create().Should().NotBeNull();
this.Invoking(_ => Create()).Should().NotThrow();
}

[Fact]
public void Ctor_StateProvider_EnsureAttached()
{
_options.StateProvider = new CircuitBreakerStateProvider();
Create();

_options.StateProvider.IsInitialized.Should().BeTrue();

_options.StateProvider.CircuitState.Should().Be(CircuitState.Closed);
_options.StateProvider.LastHandledOutcome.Should().Be(null);
}

[Fact]
public async Task Ctor_ManualControl_EnsureAttached()
{
_options.ShouldHandle.HandleException<InvalidOperationException>();
_options.ManualControl = new CircuitBreakerManualControl();
var strategy = Create();

_options.ManualControl.IsInitialized.Should().BeTrue();

await _options.ManualControl.IsolateAsync(CancellationToken.None);
strategy.Invoking(s => s.Execute(_ => { })).Should().Throw<IsolatedCircuitException>();

_behavior.Setup(v => v.OnCircuitClosed());
await _options.ManualControl.CloseAsync(CancellationToken.None);

_behavior.Setup(v => v.OnActionSuccess(CircuitState.Closed));
strategy.Invoking(s => s.Execute(_ => { })).Should().NotThrow();

_options.ManualControl.Dispose();
strategy.Invoking(s => s.Execute(_ => { })).Should().Throw<ObjectDisposedException>();

_behavior.VerifyAll();
}

[Fact]
public void Execute_HandledResult_OnFailureCalled()
{
_options.ShouldHandle.HandleResult(-1);
var strategy = Create();
var shouldBreak = false;

_behavior.Setup(v => v.OnActionFailure(CircuitState.Closed, out shouldBreak));
strategy.Execute(_ => -1).Should().Be(-1);

_behavior.VerifyAll();
}

[Fact]
public void Execute_UnhandledResult_OnActionSuccess()
{
_options.ShouldHandle.HandleResult(-1);
var strategy = Create();

_behavior.Setup(v => v.OnActionSuccess(CircuitState.Closed));
strategy.Execute(_ => 0).Should().Be(0);

_behavior.VerifyAll();
}

[Fact]
public void Execute_HandledException_OnFailureCalled()
{
_options.ShouldHandle.HandleException<InvalidOperationException>();
var strategy = Create();
var shouldBreak = false;

_behavior.Setup(v => v.OnActionFailure(CircuitState.Closed, out shouldBreak));

strategy.Invoking(s => s.Execute(_ => throw new InvalidOperationException())).Should().Throw<InvalidOperationException>();

_behavior.VerifyAll();
}

[Fact]
public void Execute_UnhandledException_NoCalls()
{
_options.ShouldHandle.HandleException<InvalidOperationException>();
var strategy = Create();

strategy.Invoking(s => s.Execute(_ => throw new ArgumentException())).Should().Throw<ArgumentException>();

_behavior.VerifyNoOtherCalls();
}

public void Dispose() => _controller.Dispose();

[Fact]
public void Execute_Ok()
{
Create().Invoking(s => s.Execute(_ => { })).Should().NotThrow();
}

private CircuitBreakerResilienceStrategy Create() => new(_timeProvider.Object, _telemetry, new CircuitBreakerStrategyOptions());
private CircuitBreakerResilienceStrategy Create() => new(_options, _controller);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Polly.CircuitBreaker;
using Polly.Strategy;

namespace Polly.Core.Tests.CircuitBreaker;

Expand All @@ -19,16 +20,16 @@ public void NotInitialized_EnsureDefaults()
var provider = new CircuitBreakerStateProvider();

provider.CircuitState.Should().Be(CircuitState.Closed);
provider.LastException.Should().Be(null);
provider.LastHandledOutcome.Should().Be(null);
}

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

await control
.Invoking(c => c.ResetAsync(CancellationToken.None))
.Invoking(c => c.CloseAsync(CancellationToken.None))
.Should()
.ThrowAsync<InvalidOperationException>();
}
Expand Down Expand Up @@ -61,11 +62,11 @@ public void Initialize_Ok()
() =>
{
exceptionCalled = true;
return new InvalidOperationException();
return new Outcome(typeof(string), new InvalidOperationException());
});

provider.CircuitState.Should().Be(CircuitState.HalfOpen);
provider.LastException.Should().BeOfType<InvalidOperationException>();
provider.LastHandledOutcome!.Value.Exception.Should().BeOfType<InvalidOperationException>();

stateCalled.Should().BeTrue();
exceptionCalled.Should().BeTrue();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Polly.CircuitBreaker;

namespace Polly.Core.Tests.CircuitBreaker.Controller;
public class AdvancedCircuitBehaviorTests
{
[Fact]
public void HappyPath()
{
var behavior = new AdvancedCircuitBehavior();

behavior
.Invoking(b =>
{
behavior.OnActionFailure(CircuitState.Closed, out var shouldBreak);
shouldBreak.Should().BeFalse();
behavior.OnCircuitClosed();
behavior.OnActionSuccess(CircuitState.Closed);
})
.Should()
.NotThrow();
}
}

0 comments on commit c7c8dbd

Please sign in to comment.