From b47b93102e3aad3a762594a8ee49aec7b9cb5770 Mon Sep 17 00:00:00 2001 From: martintmk Date: Mon, 24 Apr 2023 15:56:28 +0200 Subject: [PATCH 1/4] Introduce Fallback Resilience Strategy --- .../Internals/Helper.Pipeline.cs | 4 +- .../Fallback/FallbackHandlerTests.cs | 129 ++++++++++++++++++ ...esilienceStrategyBuilderExtensionsTests.cs | 71 ++++++++++ .../FallbackResilienceStrategyTests.cs | 121 ++++++++++++++++ .../FallbackStrategyOptionsTResultTests.cs | 77 +++++++++++ .../Fallback/FallbackStrategyOptionsTests.cs | 41 ++++++ src/Polly.Core/Fallback/FallbackConstants.cs | 10 ++ .../Fallback/FallbackHandler.Handler.cs | 34 +++++ .../Fallback/FallbackHandler.TResult.cs | 36 +++++ src/Polly.Core/Fallback/FallbackHandler.cs | 89 ++++++++++++ .../Fallback/FallbackResilienceStrategy.cs | 62 +++++++++ ...backResilienceStrategyBuilderExtensions.cs | 71 ++++++++++ .../FallbackStrategyOptions.TResult.cs | 59 ++++++++ .../Fallback/FallbackStrategyOptions.cs | 34 +++++ .../Fallback/HandleFallbackArguments.cs | 16 +++ .../Fallback/OnFallbackArguments.cs | 16 +++ .../Fallback/VoidFallbackHandler.cs | 34 +++++ 17 files changed, 901 insertions(+), 3 deletions(-) create mode 100644 src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs create mode 100644 src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs create mode 100644 src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs create mode 100644 src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTResultTests.cs create mode 100644 src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs create mode 100644 src/Polly.Core/Fallback/FallbackConstants.cs create mode 100644 src/Polly.Core/Fallback/FallbackHandler.Handler.cs create mode 100644 src/Polly.Core/Fallback/FallbackHandler.TResult.cs create mode 100644 src/Polly.Core/Fallback/FallbackHandler.cs create mode 100644 src/Polly.Core/Fallback/FallbackResilienceStrategy.cs create mode 100644 src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs create mode 100644 src/Polly.Core/Fallback/FallbackStrategyOptions.TResult.cs create mode 100644 src/Polly.Core/Fallback/FallbackStrategyOptions.cs create mode 100644 src/Polly.Core/Fallback/HandleFallbackArguments.cs create mode 100644 src/Polly.Core/Fallback/OnFallbackArguments.cs create mode 100644 src/Polly.Core/Fallback/VoidFallbackHandler.cs diff --git a/src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs b/src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs index 30ac5c7751c..54a6c77766c 100644 --- a/src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs +++ b/src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs @@ -1,6 +1,4 @@ -using System.Threading.RateLimiting; using Polly; -using Polly.Strategy; namespace Polly.Core.Benchmarks; @@ -29,7 +27,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), diff --git a/src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs b/src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs new file mode 100644 index 00000000000..4515c1a0da2 --- /dev/null +++ b/src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs @@ -0,0 +1,129 @@ +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(handler => + { + handler.FallbackAction = null!; + handler.ShouldHandle = null!; + })) + .Should() + .Throw() + .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() + .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(handler => + { + handler.FallbackAction = (_, _) => new ValueTask(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(handler => + { + handler.FallbackAction = (_, _) => new ValueTask(0); + handler.ShouldHandle.HandleResult(-1); + }) + .CreateHandler(); + + var args = new HandleFallbackArguments(ResilienceContext.Get()); + handler.Should().NotBeNull(); + var action = await handler!.ShouldHandleAsync(new Outcome(-1), args); + (await action!(new Outcome(-1), args)).Should().Be(0); + + action = await handler!.ShouldHandleAsync(new Outcome(0), args); + action.Should().BeNull(); + } + + [Fact] + public async Task SetVoidFallback_Ok() + { + var handler = new FallbackHandler() + .SetVoidFallback(handler => + { + handler.FallbackAction = (_, _) => default; + handler.ShouldHandle.HandleException(); + }) + .CreateHandler(); + + var args = new HandleFallbackArguments(ResilienceContext.Get()); + handler.Should().NotBeNull(); + var action = await handler!.ShouldHandleAsync(new Outcome(new InvalidOperationException()), args); + action.Should().NotBeNull(); + (await action!(new Outcome(new InvalidOperationException()), args)).Should().Be(VoidResult.Instance); + + action = await handler!.ShouldHandleAsync(new Outcome(new ArgumentNullException()), args); + action.Should().BeNull(); + } + + [Fact] + public async Task ShouldHandleAsync_UnknownResultType_Null() + { + var handler = new FallbackHandler() + .SetFallback(handler => + { + handler.FallbackAction = (_, _) => default; + handler.ShouldHandle.HandleException(); + }) + .SetFallback(handler => + { + handler.FallbackAction = (_, _) => default; + }) + .CreateHandler(); + + var args = new HandleFallbackArguments(ResilienceContext.Get()); + var action = await handler!.ShouldHandleAsync(new Outcome(new InvalidOperationException()), args); + action.Should().BeNull(); + } +} diff --git a/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs b/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs new file mode 100644 index 00000000000..34333d73173 --- /dev/null +++ b/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs @@ -0,0 +1,71 @@ +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> FallbackCases = new() + { + builder => + { + builder.AddFallback(new FallbackStrategyOptions()); + }, + builder => + { + builder.AddFallback(new FallbackStrategyOptions{ FallbackAction = (_, _) => new ValueTask(0) }); + }, + builder => + { + builder.AddFallback(handle => { }, (_, _) => new ValueTask(0)); + }, + }; + + [MemberData(nameof(FallbackCases))] + [Theory] + public void AddFallback_Ok(Action configure) + { + configure(_builder); + _builder.Build().Should().BeOfType(); + } + + [Fact] + public void AddFallback_Generic_Ok() + { + var strategy = _builder + .AddFallback( + handler => handler.HandleResult(-1).HandleException(), + (_, args) => + { + args.Context.Should().NotBeNull(); + return new ValueTask(1); + }) + .Build(); + + strategy.Execute(_ => -1).Should().Be(1); + strategy.Execute(_ => throw new InvalidOperationException()).Should().Be(1); + } + + [Fact] + public void AddFallback_InvalidOptions_Throws() + { + _builder + .Invoking(b => b.AddFallback(new FallbackStrategyOptions { Handler = null! })) + .Should() + .Throw() + .WithMessage("The fallback strategy options are invalid.*"); + } + + [Fact] + public void AddFallbackT_InvalidOptions_Throws() + { + _builder + .Invoking(b => b.AddFallback(new FallbackStrategyOptions { ShouldHandle = null! })) + .Should() + .Throw() + .WithMessage("The fallback strategy options are invalid.*"); + } +} diff --git a/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs b/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs new file mode 100644 index 00000000000..6bd7228c21a --- /dev/null +++ b/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs @@ -0,0 +1,121 @@ + +using Polly.Fallback; +using Polly.Strategy; + +namespace Polly.Core.Tests.Fallback; + +public class FallbackResilienceStrategyTests +{ + private readonly FallbackStrategyOptions _options = new(); + private readonly List _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(handler => + { + handler.ShouldHandle.HandleResult(-1); + handler.FallbackAction = (outcome, args) => + { + outcome.Result.Should().Be(-1); + args.Context.Should().NotBeNull(); + return new ValueTask(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(handler => + { + handler.ShouldHandle.HandleException(); + handler.FallbackAction = (outcome, args) => + { + outcome.Exception.Should().BeOfType(); + args.Context.Should().NotBeNull(); + return new ValueTask(0); + }; + }); + + Create().Execute(_ => 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(handler => + { + handler.ShouldHandle.HandleException(); + handler.FallbackAction = (_, _) => + { + fallbackActionCalled = true; + return new ValueTask(0); + }; + }); + + Create().Invoking(s => s.Execute(_ => throw new ArgumentException())).Should().Throw(); + + _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(handler => + { + handler.ShouldHandle.HandleResult(-1); + handler.FallbackAction = (_, _) => + { + fallbackActionCalled = true; + return new ValueTask(0); + }; + }); + + Create().Execute(_ => 0).Should().Be(0); + _args.Should().BeEmpty(); + called.Should().BeFalse(); + fallbackActionCalled.Should().BeFalse(); + } + + private FallbackResilienceStrategy Create() => new(_options, _telemetry); +} diff --git a/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTResultTests.cs b/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTResultTests.cs new file mode 100644 index 00000000000..c400bd6bd68 --- /dev/null +++ b/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTResultTests.cs @@ -0,0 +1,77 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Fallback; +using Polly.Strategy; +using Polly.Utils; + +namespace Polly.Core.Tests.Fallback; +public class FallbackStrategyOptionsTResultTests +{ + [Fact] + public void Ctor_EnsureDefaults() + { + var options = new FallbackStrategyOptions(); + + options.StrategyType.Should().Be("Fallback"); + options.ShouldHandle.Should().NotBeNull(); + options.ShouldHandle.IsEmpty.Should().BeTrue(); + options.OnFallback.IsEmpty.Should().BeTrue(); + } + + [Fact] + public void Validation() + { + var options = new FallbackStrategyOptions + { + OnFallback = null!, + ShouldHandle = null! + }; + + options + .Invoking(o => ValidationHelper.ValidateObject(o, "Invalid.")) + .Should() + .Throw() + .WithMessage(""" + Invalid. + + Validation Errors: + The ShouldHandle field is required. + The FallbackAction field is required. + The OnFallback field is required. + """); + } + + [Fact] + public async Task AsNonGenericOptions_Ok() + { + var called = false; + var options = new FallbackStrategyOptions + { + OnFallback = new OutcomeEvent().Register(() => called = true), + ShouldHandle = new OutcomePredicate().HandleResult(-1), + FallbackAction = (_, _) => new ValueTask(1), + StrategyName = "Dummy", + StrategyType = "Dummy-Fallback" + }; + + var nonGeneric = options.AsNonGenericOptions(); + + nonGeneric.Should().NotBeNull(); + nonGeneric.StrategyType.Should().Be("Dummy-Fallback"); + nonGeneric.StrategyName.Should().Be("Dummy"); + nonGeneric.Handler.IsEmpty.Should().BeFalse(); + + var handler = nonGeneric.Handler.CreateHandler(); + handler.Should().NotBeNull(); + + var result = await handler!.ShouldHandleAsync(new Outcome(-1), new HandleFallbackArguments(ResilienceContext.Get())); + result.Should().Be(options.FallbackAction); + + result = await handler!.ShouldHandleAsync(new Outcome(0), new HandleFallbackArguments(ResilienceContext.Get())); + result.Should().BeNull(); + + nonGeneric.OnFallback.IsEmpty.Should().BeFalse(); + await nonGeneric.OnFallback.CreateHandler()!.HandleAsync(new Outcome(0), new OnFallbackArguments(ResilienceContext.Get())); + + called.Should().BeTrue(); + } +} diff --git a/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs b/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs new file mode 100644 index 00000000000..c5db393c6d2 --- /dev/null +++ b/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Fallback; +using Polly.Utils; + +namespace Polly.Core.Tests.Fallback; + +public class FallbackStrategyOptionsTests +{ + [Fact] + public void Ctor_EnsureDefaults() + { + var options = new FallbackStrategyOptions(); + + options.StrategyType.Should().Be("Fallback"); + options.Handler.Should().NotBeNull(); + options.Handler.IsEmpty.Should().BeTrue(); + options.OnFallback.IsEmpty.Should().BeTrue(); + } + + [Fact] + public void Validation() + { + var options = new FallbackStrategyOptions + { + OnFallback = null!, + Handler = null! + }; + + options + .Invoking(o => ValidationHelper.ValidateObject(o, "Invalid.")) + .Should() + .Throw() + .WithMessage(""" + Invalid. + + Validation Errors: + The ShouldHandle field is required. + The OnFallback field is required. + """); + } +} diff --git a/src/Polly.Core/Fallback/FallbackConstants.cs b/src/Polly.Core/Fallback/FallbackConstants.cs new file mode 100644 index 00000000000..832055d3230 --- /dev/null +++ b/src/Polly.Core/Fallback/FallbackConstants.cs @@ -0,0 +1,10 @@ +namespace Polly.Fallback; + +internal static class FallbackConstants +{ + public const string OnFallback = "OnFallback"; + + public const string StrategyType = "Fallback"; + +} + diff --git a/src/Polly.Core/Fallback/FallbackHandler.Handler.cs b/src/Polly.Core/Fallback/FallbackHandler.Handler.cs new file mode 100644 index 00000000000..7e63b81871f --- /dev/null +++ b/src/Polly.Core/Fallback/FallbackHandler.Handler.cs @@ -0,0 +1,34 @@ +using Polly.Strategy; + +namespace Polly.Fallback; + +public partial class FallbackHandler +{ + internal sealed class Handler + { + private readonly OutcomePredicate.Handler _handler; + private readonly Dictionary _actions; + + internal Handler(OutcomePredicate.Handler handler, Dictionary generators) + { + _handler = handler; + _actions = generators; + } + + public async ValueTask?> ShouldHandleAsync(Outcome outcome, HandleFallbackArguments arguments) + { + if (!_actions.TryGetValue(typeof(TResult), out var action)) + { + return null; + } + + if (!await _handler.ShouldHandleAsync(outcome, arguments).ConfigureAwait(arguments.Context.ContinueOnCapturedContext)) + { + return null; + } + + return (FallbackAction)action; + } + } +} + diff --git a/src/Polly.Core/Fallback/FallbackHandler.TResult.cs b/src/Polly.Core/Fallback/FallbackHandler.TResult.cs new file mode 100644 index 00000000000..4dbb25fdf9f --- /dev/null +++ b/src/Polly.Core/Fallback/FallbackHandler.TResult.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.Fallback; + +/// +/// Represents an asynchronous delegate for handling fallback actions. +/// +/// The result type. +/// The of the operation. +/// Supplementary for the fallback action. +/// A containing the TResult. +public delegate ValueTask FallbackAction(Outcome outcome, HandleFallbackArguments arguments); + +/// +/// Encompasses logic for managing fallback operations for a single result type. +/// +/// The result type. +/// +/// Every fallback handler requires a predicate that determines whether a fallback should be performed for a given result and also +/// the fallback action to execute. +/// +public class FallbackHandler +{ + /// + /// Gets or sets the predicate that determines whether a fallback should be performed for a given result. + /// + [Required] + public OutcomePredicate ShouldHandle { get; set; } = new(); + + /// + /// Gets or sets the fallback action to be executed if predicate evaluates as true. + /// + [Required] + public FallbackAction? FallbackAction { get; set; } = null; +} diff --git a/src/Polly.Core/Fallback/FallbackHandler.cs b/src/Polly.Core/Fallback/FallbackHandler.cs new file mode 100644 index 00000000000..cdc5ccece91 --- /dev/null +++ b/src/Polly.Core/Fallback/FallbackHandler.cs @@ -0,0 +1,89 @@ +using Polly.Strategy; + +namespace Polly.Fallback; + +/// +/// Represents a class for managing fallback handlers. +/// +public partial class FallbackHandler +{ + private readonly OutcomePredicate _predicates = new(); + private readonly Dictionary _actions = new(); + + /// + /// Gets a value indicating whether the fallback handler is empty. + /// + public bool IsEmpty => _predicates.IsEmpty; + + /// + /// Configures a fallback handler for a specific result type. + /// + /// The result type. + /// An action that configures the fallback handler instance for a specific result. + /// The current instance. + public FallbackHandler SetFallback(Action> configure) + { + Guard.NotNull(configure); + + var handler = new FallbackHandler(); + configure(handler); + + ValidationHelper.ValidateObject(handler, "The fallback handler configuration is invalid."); + + if (handler.ShouldHandle.IsEmpty) + { + return this; + } + + _predicates.SetPredicates(handler.ShouldHandle); + _actions[typeof(TResult)] = handler.FallbackAction!; + + return this; + } + + /// + /// Configures a void-based fallback handler. + /// + /// An action that configures the void-based fallback handler. + /// The current instance. + public FallbackHandler SetVoidFallback(Action configure) + { + Guard.NotNull(configure); + + var handler = new VoidFallbackHandler(); + configure(handler); + + ValidationHelper.ValidateObject(handler, "The fallback handler configuration is invalid."); + + if (handler.ShouldHandle.IsEmpty) + { + return this; + } + + _predicates.SetVoidPredicates(handler.ShouldHandle); + _actions[typeof(VoidResult)] = CreateGenericAction(handler.FallbackAction!); + + return this; + } + + internal Handler? CreateHandler() + { + var shouldHandle = _predicates.CreateHandler(); + if (shouldHandle == null || _actions.Count == 0) + { + return null; + } + + return new Handler(shouldHandle, _actions); + } + + private static FallbackAction CreateGenericAction(FallbackAction action) + { + return async (outcome, args) => + { + await action(outcome.AsOutcome(), args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + return VoidResult.Instance; + }; + } +} + diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs new file mode 100644 index 00000000000..12ec72ed532 --- /dev/null +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Polly.Strategy; + +namespace Polly.Fallback; + +internal sealed class FallbackResilienceStrategy : ResilienceStrategy +{ + private readonly FallbackHandler.Handler? _handler; + private readonly OutcomeEvent.Handler? _onFallback; + private readonly ResilienceStrategyTelemetry _telemetry; + + public FallbackResilienceStrategy(FallbackStrategyOptions options, ResilienceStrategyTelemetry telemetry) + { + _handler = options.Handler.CreateHandler(); + _onFallback = options.OnFallback.CreateHandler(); + _telemetry = telemetry; + } + + protected internal override async ValueTask ExecuteCoreAsync(Func> callback, ResilienceContext context, TState state) + { + if (_handler == null) + { + return await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + + Outcome outcome; + var args = new HandleFallbackArguments(context); + FallbackAction? action; + + try + { + var result = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + outcome = new Outcome(result); + action = await _handler.ShouldHandleAsync(outcome, args).ConfigureAwait(context.ContinueOnCapturedContext); + + if (action == null) + { + return result; + } + } + catch (Exception e) + { + outcome = new Outcome(e); + action = await _handler.ShouldHandleAsync(outcome, args).ConfigureAwait(context.ContinueOnCapturedContext); + + if (action == null) + { + throw; + } + } + + _telemetry.Report(FallbackConstants.OnFallback, outcome, args); + + if (_onFallback != null) + { + await _onFallback.HandleAsync(outcome, new OnFallbackArguments(context)).ConfigureAwait(context.ContinueOnCapturedContext); + } + + return await action(outcome, args).ConfigureAwait(context.ContinueOnCapturedContext); + } +} diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs new file mode 100644 index 00000000000..d3ff52c4dc4 --- /dev/null +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs @@ -0,0 +1,71 @@ +using System; +using Polly.Fallback; +using Polly.Strategy; + +namespace Polly; + +/// +/// Provides extension methods for configuring fallback resilience strategies for . +/// +public static class FallbackResilienceStrategyBuilderExtensions +{ + /// + /// Adds a fallback resilience strategy for a specific type to the builder. + /// + /// The result type. + /// The resilience strategy builder. + /// An action to configure the fallback predicate. + /// The fallback action to be executed. + /// The builder instance with the fallback strategy added. + public static ResilienceStrategyBuilder AddFallback( + this ResilienceStrategyBuilder builder, + Action> shouldHandle, + FallbackAction fallbackAction) + { + Guard.NotNull(builder); + Guard.NotNull(shouldHandle); + Guard.NotNull(fallbackAction); + + var options = new FallbackStrategyOptions + { + FallbackAction = fallbackAction, + }; + + shouldHandle(options.ShouldHandle); + + return builder.AddFallback(options); + } + + /// + /// Adds a fallback resilience strategy with the provided options to the builder. + /// + /// The result type. + /// The resilience strategy builder. + /// The options to configure the fallback resilience strategy. + /// The builder instance with the fallback strategy added. + public static ResilienceStrategyBuilder AddFallback(this ResilienceStrategyBuilder builder, FallbackStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The fallback strategy options are invalid."); + + return builder.AddFallback(options.AsNonGenericOptions()); + } + + /// + /// Adds a fallback resilience strategy with the provided options to the builder. + /// + /// The resilience strategy builder. + /// The options to configure the fallback resilience strategy. + /// The builder instance with the fallback strategy added. + public static ResilienceStrategyBuilder AddFallback(this ResilienceStrategyBuilder builder, FallbackStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The fallback strategy options are invalid."); + + return builder.AddStrategy(context => new FallbackResilienceStrategy(options, context.Telemetry), options); + } +} diff --git a/src/Polly.Core/Fallback/FallbackStrategyOptions.TResult.cs b/src/Polly.Core/Fallback/FallbackStrategyOptions.TResult.cs new file mode 100644 index 00000000000..69c3cb47074 --- /dev/null +++ b/src/Polly.Core/Fallback/FallbackStrategyOptions.TResult.cs @@ -0,0 +1,59 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.Fallback; + +/// +/// Represents the options for configuring a fallback resilience strategy with a specific result type. +/// +/// The result type. +public class FallbackStrategyOptions : ResilienceStrategyOptions +{ + /// + /// Initializes a new instance of the class. + /// + public FallbackStrategyOptions() => StrategyType = FallbackConstants.StrategyType; + + /// + /// Gets or sets the outcome predicate for determining whether a fallback should be executed. + /// + /// + /// This property is required. + /// + [Required] + public OutcomePredicate ShouldHandle { get; set; } = new(); + + /// + /// Gets or sets the fallback action to be executed if the predicate evaluates as true. + /// + /// + /// This property is required. Defaults to null. + /// + [Required] + public FallbackAction? FallbackAction { get; set; } + + /// + /// Gets or sets the outcome event instance responsible for triggering fallback events. + /// + /// + /// This property is required. + /// + [Required] + public OutcomeEvent OnFallback { get; set; } = new(); + + internal FallbackStrategyOptions AsNonGenericOptions() + { + return new FallbackStrategyOptions + { + StrategyType = StrategyType, + StrategyName = StrategyName, + OnFallback = new OutcomeEvent().SetCallbacks(OnFallback), + Handler = new FallbackHandler().SetFallback(handler => + { + handler.ShouldHandle = ShouldHandle; + handler.FallbackAction = FallbackAction; + }) + }; + } +} + diff --git a/src/Polly.Core/Fallback/FallbackStrategyOptions.cs b/src/Polly.Core/Fallback/FallbackStrategyOptions.cs new file mode 100644 index 00000000000..c5680606b29 --- /dev/null +++ b/src/Polly.Core/Fallback/FallbackStrategyOptions.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.Fallback; + +/// +/// Represents the options for configuring a fallback resilience strategy. +/// +public class FallbackStrategyOptions : ResilienceStrategyOptions +{ + /// + /// Initializes a new instance of the class. + /// + public FallbackStrategyOptions() => StrategyType = FallbackConstants.StrategyType; + + /// + /// Gets or sets the instance used for configure fallback scenarios. + /// + /// + /// This property is required. + /// + [Required] + public FallbackHandler Handler { get; set; } = new(); + + /// + /// Gets or sets the outcome event instance for raising fallback events. + /// + /// + /// This property is required. + /// + [Required] + public OutcomeEvent OnFallback { get; set; } = new(); +} + diff --git a/src/Polly.Core/Fallback/HandleFallbackArguments.cs b/src/Polly.Core/Fallback/HandleFallbackArguments.cs new file mode 100644 index 00000000000..f0ba1080ee2 --- /dev/null +++ b/src/Polly.Core/Fallback/HandleFallbackArguments.cs @@ -0,0 +1,16 @@ +using Polly.Strategy; + +namespace Polly.Fallback; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Represents arguments used in fallback handling scenarios. +/// +public readonly struct HandleFallbackArguments : IResilienceArguments +{ + internal HandleFallbackArguments(ResilienceContext context) => Context = context; + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Fallback/OnFallbackArguments.cs b/src/Polly.Core/Fallback/OnFallbackArguments.cs new file mode 100644 index 00000000000..267215ea6d2 --- /dev/null +++ b/src/Polly.Core/Fallback/OnFallbackArguments.cs @@ -0,0 +1,16 @@ +using Polly.Strategy; + +namespace Polly.Fallback; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Represents arguments used when the fallback is about to be executed. +/// +public readonly struct OnFallbackArguments : IResilienceArguments +{ + internal OnFallbackArguments(ResilienceContext context) => Context = context; + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Fallback/VoidFallbackHandler.cs b/src/Polly.Core/Fallback/VoidFallbackHandler.cs new file mode 100644 index 00000000000..0594fffde20 --- /dev/null +++ b/src/Polly.Core/Fallback/VoidFallbackHandler.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.Fallback; + +/// +/// Represents an asynchronous delegate for handling void-based fallback actions. +/// +/// The of the operation. +/// Supplementary for the fallback action. +/// A representing the asynchronous operation. +public delegate ValueTask FallbackAction(Outcome outcome, HandleFallbackArguments arguments); + +/// +/// Encompasses logic for managing fallback operations with void-based results. +/// +/// +/// Every fallback handler requires a predicate that determines whether a fallback should be performed for a given +/// void-based result and also the fallback action to execute. +/// +public class VoidFallbackHandler +{ + /// + /// Gets or sets the predicate that determines whether a fallback should be handled. + /// + [Required] + public VoidOutcomePredicate ShouldHandle { get; set; } = new(); + + /// + /// Gets or sets the fallback action to be executed if the predicate evaluates as true. + /// + [Required] + public FallbackAction? FallbackAction { get; set; } = null; +} From 1745952bed5ff5fc068d8b5b3896269803b40c92 Mon Sep 17 00:00:00 2001 From: martintmk Date: Tue, 25 Apr 2023 15:11:46 +0200 Subject: [PATCH 2/4] fixes --- src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs | 2 ++ .../Fallback/FallbackResilienceStrategyTests.cs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs b/src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs index 54a6c77766c..af825f82d80 100644 --- a/src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs +++ b/src/Polly.Core.Benchmarks/Internals/Helper.Pipeline.cs @@ -1,4 +1,6 @@ +using System.Threading.RateLimiting; using Polly; +using Polly.Strategy; namespace Polly.Core.Benchmarks; diff --git a/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs b/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs index 6bd7228c21a..5f848d70055 100644 --- a/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs +++ b/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs @@ -1,4 +1,3 @@ - using Polly.Fallback; using Polly.Strategy; From 906217565b157ecc01825d3b0d2dcda913fcf509 Mon Sep 17 00:00:00 2001 From: martintmk Date: Tue, 25 Apr 2023 16:35:28 +0200 Subject: [PATCH 3/4] PR comments and fixes --- src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs | 1 + .../FallbackResilienceStrategyBuilderExtensionsTests.cs | 1 + .../Fallback/FallbackStrategyOptionsTResultTests.cs | 1 + src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs b/src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs index 4515c1a0da2..1db6cc247f4 100644 --- a/src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs +++ b/src/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs @@ -3,6 +3,7 @@ using Polly.Strategy; namespace Polly.Core.Tests.Fallback; + public class FallbackHandlerTests { [Fact] diff --git a/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs b/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs index 34333d73173..e0aebc2f3a4 100644 --- a/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs +++ b/src/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs @@ -4,6 +4,7 @@ using Polly.Fallback; namespace Polly.Core.Tests.Fallback; + public class FallbackResilienceStrategyBuilderExtensionsTests { private readonly ResilienceStrategyBuilder _builder = new(); diff --git a/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTResultTests.cs b/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTResultTests.cs index c400bd6bd68..b9833a53d0e 100644 --- a/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTResultTests.cs +++ b/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTResultTests.cs @@ -4,6 +4,7 @@ using Polly.Utils; namespace Polly.Core.Tests.Fallback; + public class FallbackStrategyOptionsTResultTests { [Fact] diff --git a/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs b/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs index c5db393c6d2..0627a85d2c8 100644 --- a/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs +++ b/src/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs @@ -34,7 +34,7 @@ public void Validation() Invalid. Validation Errors: - The ShouldHandle field is required. + The Handler field is required. The OnFallback field is required. """); } From 1dfc954f21f399d8dd642f16016df7fbea3cea0f Mon Sep 17 00:00:00 2001 From: martintmk Date: Tue, 25 Apr 2023 17:13:20 +0200 Subject: [PATCH 4/4] Kill mutant --- src/Polly.Core/Fallback/FallbackHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Polly.Core/Fallback/FallbackHandler.cs b/src/Polly.Core/Fallback/FallbackHandler.cs index cdc5ccece91..d9a5460a18f 100644 --- a/src/Polly.Core/Fallback/FallbackHandler.cs +++ b/src/Polly.Core/Fallback/FallbackHandler.cs @@ -69,7 +69,7 @@ public FallbackHandler SetVoidFallback(Action configure) internal Handler? CreateHandler() { var shouldHandle = _predicates.CreateHandler(); - if (shouldHandle == null || _actions.Count == 0) + if (shouldHandle == null) { return null; }