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 Fallback Resilience Strategy #1158

Merged
merged 4 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ internal static partial class Helper
3,
TimeSpan.FromSeconds(1))
.AddTimeout(TimeSpan.FromSeconds(1))
.AddAdvancedCircuitBreaker(new AdvancedCircuitBreakerStrategyOptions
.AddAdvancedCircuitBreaker(new()
{
FailureThreshold = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
Expand Down
130 changes: 130 additions & 0 deletions src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.ComponentModel.DataAnnotations;
using Polly.Fallback;
using Polly.Strategy;

namespace Polly.Core.Tests.Fallback;

public class FallbackHandlerTests
{
[Fact]
public void SetFallback_ConfigureAsInvalid_Throws()
{
var handler = new FallbackHandler();

handler
.Invoking(h => h.SetFallback<int>(handler =>
{
handler.FallbackAction = null!;
handler.ShouldHandle = null!;
}))
.Should()
.Throw<ValidationException>()
.WithMessage("""
The fallback handler configuration is invalid.

Validation Errors:
The ShouldHandle field is required.
The FallbackAction field is required.
""");
}

[Fact]
public void SetVoidFallback_ConfigureAsInvalid_Throws()
{
var handler = new FallbackHandler();

handler
.Invoking(h => h.SetVoidFallback(handler =>
{
handler.FallbackAction = null!;
handler.ShouldHandle = null!;
}))
.Should()
.Throw<ValidationException>()
.WithMessage("""
The fallback handler configuration is invalid.

Validation Errors:
The ShouldHandle field is required.
The FallbackAction field is required.
""");
}

[Fact]
public void SetFallback_Empty_Discarded()
{
var handler = new FallbackHandler()
.SetFallback<int>(handler =>
{
handler.FallbackAction = (_, _) => new ValueTask<int>(0);
})
.SetVoidFallback(handler =>
{
handler.FallbackAction = (_, _) => default;
});

handler.IsEmpty.Should().BeTrue();
handler.CreateHandler().Should().BeNull();
}

[Fact]
public async Task SetFallback_Ok()
{
var handler = new FallbackHandler()
.SetFallback<int>(handler =>
{
handler.FallbackAction = (_, _) => new ValueTask<int>(0);
handler.ShouldHandle.HandleResult(-1);
})
.CreateHandler();

var args = new HandleFallbackArguments(ResilienceContext.Get());
handler.Should().NotBeNull();
var action = await handler!.ShouldHandleAsync(new Outcome<int>(-1), args);
(await action!(new Outcome<int>(-1), args)).Should().Be(0);

action = await handler!.ShouldHandleAsync(new Outcome<int>(0), args);
action.Should().BeNull();
}

[Fact]
public async Task SetVoidFallback_Ok()
{
var handler = new FallbackHandler()
.SetVoidFallback(handler =>
{
handler.FallbackAction = (_, _) => default;
handler.ShouldHandle.HandleException<InvalidOperationException>();
})
.CreateHandler();

var args = new HandleFallbackArguments(ResilienceContext.Get());
handler.Should().NotBeNull();
var action = await handler!.ShouldHandleAsync(new Outcome<VoidResult>(new InvalidOperationException()), args);
action.Should().NotBeNull();
(await action!(new Outcome<VoidResult>(new InvalidOperationException()), args)).Should().Be(VoidResult.Instance);

action = await handler!.ShouldHandleAsync(new Outcome<VoidResult>(new ArgumentNullException()), args);
action.Should().BeNull();
}

[Fact]
public async Task ShouldHandleAsync_UnknownResultType_Null()
{
var handler = new FallbackHandler()
.SetFallback<int>(handler =>
{
handler.FallbackAction = (_, _) => default;
handler.ShouldHandle.HandleException<InvalidOperationException>();
})
.SetFallback<string>(handler =>
{
handler.FallbackAction = (_, _) => default;
})
.CreateHandler();

var args = new HandleFallbackArguments(ResilienceContext.Get());
var action = await handler!.ShouldHandleAsync(new Outcome<double>(new InvalidOperationException()), args);
action.Should().BeNull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Polly.Fallback;

namespace Polly.Core.Tests.Fallback;

public class FallbackResilienceStrategyBuilderExtensionsTests
{
private readonly ResilienceStrategyBuilder _builder = new();

public static readonly TheoryData<Action<ResilienceStrategyBuilder>> FallbackCases = new()
{
builder =>
{
builder.AddFallback(new FallbackStrategyOptions());
},
builder =>
{
builder.AddFallback(new FallbackStrategyOptions<double>{ FallbackAction = (_, _) => new ValueTask<double>(0) });
},
builder =>
{
builder.AddFallback<double>(handle => { }, (_, _) => new ValueTask<double>(0));
},
};

[MemberData(nameof(FallbackCases))]
[Theory]
public void AddFallback_Ok(Action<ResilienceStrategyBuilder> configure)
{
configure(_builder);
_builder.Build().Should().BeOfType<FallbackResilienceStrategy>();
}

[Fact]
public void AddFallback_Generic_Ok()
{
var strategy = _builder
.AddFallback<int>(
handler => handler.HandleResult(-1).HandleException<InvalidOperationException>(),
(_, args) =>
{
args.Context.Should().NotBeNull();
return new ValueTask<int>(1);
})
.Build();

strategy.Execute(_ => -1).Should().Be(1);
strategy.Execute<int>(_ => throw new InvalidOperationException()).Should().Be(1);
}

[Fact]
public void AddFallback_InvalidOptions_Throws()
{
_builder
.Invoking(b => b.AddFallback(new FallbackStrategyOptions { Handler = null! }))
.Should()
.Throw<ValidationException>()
.WithMessage("The fallback strategy options are invalid.*");
}

[Fact]
public void AddFallbackT_InvalidOptions_Throws()
{
_builder
.Invoking(b => b.AddFallback(new FallbackStrategyOptions<double> { ShouldHandle = null! }))
.Should()
.Throw<ValidationException>()
.WithMessage("The fallback strategy options are invalid.*");
}
}
120 changes: 120 additions & 0 deletions src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Polly.Fallback;
using Polly.Strategy;

namespace Polly.Core.Tests.Fallback;

public class FallbackResilienceStrategyTests
{
private readonly FallbackStrategyOptions _options = new();
private readonly List<IResilienceArguments> _args = new();
private readonly ResilienceStrategyTelemetry _telemetry;

public FallbackResilienceStrategyTests() => _telemetry = TestUtilities.CreateResilienceTelemetry(args => _args.Add(args));

[Fact]
public void Ctor_Ok()
{
Create().Should().NotBeNull();
}

[Fact]
public void NoHandler_Skips()
{
Create().Execute(_ => { });

_args.Should().BeEmpty();
}

[Fact]
public void Handle_Result_Ok()
{
var called = false;
_options.OnFallback.Register(() => called = true);
_options.Handler.SetFallback<int>(handler =>
{
handler.ShouldHandle.HandleResult(-1);
handler.FallbackAction = (outcome, args) =>
{
outcome.Result.Should().Be(-1);
args.Context.Should().NotBeNull();
return new ValueTask<int>(0);
};
});

Create().Execute(_ => -1).Should().Be(0);

_args.Should().ContainSingle(v => v is HandleFallbackArguments);
called.Should().BeTrue();
}

[Fact]
public void Handle_Exception_Ok()
{
var called = false;
_options.OnFallback.Register(() => called = true);
_options.Handler.SetFallback<int>(handler =>
{
handler.ShouldHandle.HandleException<InvalidOperationException>();
handler.FallbackAction = (outcome, args) =>
{
outcome.Exception.Should().BeOfType<InvalidOperationException>();
args.Context.Should().NotBeNull();
return new ValueTask<int>(0);
};
});

Create().Execute<int>(_ => throw new InvalidOperationException()).Should().Be(0);

_args.Should().ContainSingle(v => v is HandleFallbackArguments);
called.Should().BeTrue();
}

[Fact]
public void Handle_UnhandledException_Ok()
{
var called = false;
var fallbackActionCalled = false;

_options.OnFallback.Register(() => called = true);
_options.Handler.SetFallback<int>(handler =>
{
handler.ShouldHandle.HandleException<InvalidOperationException>();
handler.FallbackAction = (_, _) =>
{
fallbackActionCalled = true;
return new ValueTask<int>(0);
};
});

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

_args.Should().BeEmpty();
called.Should().BeFalse();
fallbackActionCalled.Should().BeFalse();
}

[Fact]
public void Handle_UnhandledResult_Ok()
{
var called = false;
var fallbackActionCalled = false;

_options.OnFallback.Register(() => called = true);
_options.Handler.SetFallback<int>(handler =>
{
handler.ShouldHandle.HandleResult(-1);
handler.FallbackAction = (_, _) =>
{
fallbackActionCalled = true;
return new ValueTask<int>(0);
};
});

Create().Execute(_ => 0).Should().Be(0);
_args.Should().BeEmpty();
called.Should().BeFalse();
fallbackActionCalled.Should().BeFalse();
}

private FallbackResilienceStrategy Create() => new(_options, _telemetry);
}
Loading