Skip to content

Commit

Permalink
Introduce Fallback Resilience Strategy (#1158)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk committed Apr 25, 2023
1 parent a1ef1bb commit 6be94b1
Show file tree
Hide file tree
Showing 17 changed files with 903 additions and 1 deletion.
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);
}

0 comments on commit 6be94b1

Please sign in to comment.