From 38ae8fd5b94e5a6fc45a9d628a616845061baaa7 Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Wed, 26 Apr 2023 12:59:22 +0200 Subject: [PATCH] Introduce public API for Hedging Resilience Strategy (#1160) --- .../Hedging/HandleHedgingArgumentsTests.cs | 14 ++ .../Hedging/HedgingDelayArgumentsTests.cs | 15 ++ .../Hedging/HedgingHandlerTests.cs | 160 ++++++++++++++++++ ...esilienceStrategyBuilderExtensionsTests.cs | 54 ++++++ .../Hedging/HedgingResilienceStrategyTests.cs | 59 +++++++ .../HedgingStrategyOptionsTResultTests.cs | 98 +++++++++++ .../Hedging/HedgingStrategyOptionsTests.cs | 49 ++++++ .../Fallback/FallbackHandler.Handler.cs | 2 +- .../Fallback/FallbackHandler.TResult.cs | 2 +- src/Polly.Core/Fallback/FallbackHandler.cs | 2 +- .../Fallback/VoidFallbackHandler.cs | 2 +- .../Hedging/HandleHedgingArguments.cs | 16 ++ .../Hedging/HedgingActionGenerator.TResult.cs | 13 ++ .../Hedging/HedgingActionGenerator.cs | 12 ++ ...HedgingActionGeneratorArguments.TResult.cs | 18 ++ .../HedgingActionGeneratorArguments.cs | 16 ++ src/Polly.Core/Hedging/HedgingConstants.cs | 14 ++ .../Hedging/HedgingDelayArguments.cs | 25 +++ .../Hedging/HedgingHandler.Handler.cs | 33 ++++ .../Hedging/HedgingHandler.TResult.cs | 30 ++++ src/Polly.Core/Hedging/HedgingHandler.cs | 94 ++++++++++ .../Hedging/HedgingResilienceStrategy.cs | 38 +++++ ...gingResilienceStrategyBuilderExtensions.cs | 42 +++++ .../Hedging/HedgingStrategyOptions.TResult.cs | 92 ++++++++++ .../Hedging/HedgingStrategyOptions.cs | 74 ++++++++ src/Polly.Core/Hedging/OnHedgingArguments.cs | 25 +++ src/Polly.Core/Hedging/VoidHedgingHandler.cs | 29 ++++ src/Polly.Core/Strategy/NoOutcomeGenerator.cs | 5 + 28 files changed, 1029 insertions(+), 4 deletions(-) create mode 100644 src/Polly.Core.Tests/Hedging/HandleHedgingArgumentsTests.cs create mode 100644 src/Polly.Core.Tests/Hedging/HedgingDelayArgumentsTests.cs create mode 100644 src/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs create mode 100644 src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyBuilderExtensionsTests.cs create mode 100644 src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs create mode 100644 src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs create mode 100644 src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs create mode 100644 src/Polly.Core/Hedging/HandleHedgingArguments.cs create mode 100644 src/Polly.Core/Hedging/HedgingActionGenerator.TResult.cs create mode 100644 src/Polly.Core/Hedging/HedgingActionGenerator.cs create mode 100644 src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs create mode 100644 src/Polly.Core/Hedging/HedgingActionGeneratorArguments.cs create mode 100644 src/Polly.Core/Hedging/HedgingConstants.cs create mode 100644 src/Polly.Core/Hedging/HedgingDelayArguments.cs create mode 100644 src/Polly.Core/Hedging/HedgingHandler.Handler.cs create mode 100644 src/Polly.Core/Hedging/HedgingHandler.TResult.cs create mode 100644 src/Polly.Core/Hedging/HedgingHandler.cs create mode 100644 src/Polly.Core/Hedging/HedgingResilienceStrategy.cs create mode 100644 src/Polly.Core/Hedging/HedgingResilienceStrategyBuilderExtensions.cs create mode 100644 src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs create mode 100644 src/Polly.Core/Hedging/HedgingStrategyOptions.cs create mode 100644 src/Polly.Core/Hedging/OnHedgingArguments.cs create mode 100644 src/Polly.Core/Hedging/VoidHedgingHandler.cs diff --git a/src/Polly.Core.Tests/Hedging/HandleHedgingArgumentsTests.cs b/src/Polly.Core.Tests/Hedging/HandleHedgingArgumentsTests.cs new file mode 100644 index 00000000000..abb637adee7 --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HandleHedgingArgumentsTests.cs @@ -0,0 +1,14 @@ +using Polly.Hedging; + +namespace Polly.Core.Tests.Hedging; + +public class HandleHedgingArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new HandleHedgingArguments(ResilienceContext.Get()); + + args.Context.Should().NotBeNull(); + } +} diff --git a/src/Polly.Core.Tests/Hedging/HedgingDelayArgumentsTests.cs b/src/Polly.Core.Tests/Hedging/HedgingDelayArgumentsTests.cs new file mode 100644 index 00000000000..ec882995109 --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingDelayArgumentsTests.cs @@ -0,0 +1,15 @@ +using Polly.Hedging; + +namespace Polly.Core.Tests.Hedging; + +public class HedgingDelayArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new HedgingDelayArguments(ResilienceContext.Get(), 5); + + args.Context.Should().NotBeNull(); + args.Attempt.Should().Be(5); + } +} diff --git a/src/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs b/src/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs new file mode 100644 index 00000000000..78e889ea54b --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs @@ -0,0 +1,160 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Hedging; +using Polly.Strategy; + +namespace Polly.Core.Tests.Hedging; + +public class HedgingHandlerTests +{ + [Fact] + public void SetHedging_ConfigureAsInvalid_Throws() + { + var handler = new HedgingHandler(); + + handler + .Invoking(h => h.SetHedging(handler => + { + handler.HedgingActionGenerator = null!; + handler.ShouldHandle = null!; + })) + .Should() + .Throw() + .WithMessage(""" + The hedging handler configuration is invalid. + + Validation Errors: + The ShouldHandle field is required. + The HedgingActionGenerator field is required. + """); + } + + [Fact] + public void SetVoidHedging_ConfigureAsInvalid_Throws() + { + var handler = new HedgingHandler(); + + handler + .Invoking(h => h.SetVoidHedging(handler => + { + handler.HedgingActionGenerator = null!; + handler.ShouldHandle = null!; + })) + .Should() + .Throw() + .WithMessage(""" + The hedging handler configuration is invalid. + + Validation Errors: + The ShouldHandle field is required. + The HedgingActionGenerator field is required. + """); + } + + [Fact] + public void SetHedging_Empty_Discarded() + { + var handler = new HedgingHandler() + .SetHedging(handler => + { + handler.HedgingActionGenerator = args => () => Task.FromResult(10); + }) + .SetVoidHedging(handler => + { + handler.HedgingActionGenerator = args => () => Task.CompletedTask; + }); + + handler.IsEmpty.Should().BeTrue(); + handler.CreateHandler().Should().BeNull(); + } + + [Fact] + public async Task SetHedging_Ok() + { + var handler = new HedgingHandler() + .SetHedging(handler => + { + handler.HedgingActionGenerator = args => () => Task.FromResult(0); + handler.ShouldHandle.HandleResult(-1); + }) + .CreateHandler(); + + var args = new HandleHedgingArguments(ResilienceContext.Get()); + handler.Should().NotBeNull(); + var result = await handler!.ShouldHandleAsync(new Outcome(-1), args); + result.Should().BeTrue(); + + handler.HandlesHedging().Should().BeTrue(); + + var action = handler.TryCreateHedgedAction(ResilienceContext.Get()); + action.Should().NotBeNull(); + (await (action!()!)).Should().Be(0); + + handler.TryCreateHedgedAction(ResilienceContext.Get()).Should().BeNull(); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task SetVoidHedging_Ok(bool returnsNullAction) + { + var handler = new HedgingHandler() + .SetVoidHedging(handler => + { + handler.HedgingActionGenerator = args => + { + args.Context.Should().NotBeNull(); + if (returnsNullAction) + { + return null; + } + + return () => Task.CompletedTask; + }; + handler.ShouldHandle.HandleException(); + }) + .CreateHandler(); + + var args = new HandleHedgingArguments(ResilienceContext.Get()); + handler.Should().NotBeNull(); + var result = await handler!.ShouldHandleAsync(new Outcome(new InvalidOperationException()), args); + result.Should().BeTrue(); + + handler.HandlesHedging().Should().BeTrue(); + + var action = handler.TryCreateHedgedAction(ResilienceContext.Get()); + if (returnsNullAction) + { + action.Should().BeNull(); + } + else + { + action.Should().NotBeNull(); + (await (action!()!)).Should().Be(VoidResult.Instance); + } + } + + [Fact] + public async Task ShouldHandleAsync_UnknownResultType_Null() + { + var handler = new HedgingHandler() + .SetHedging(handler => + { + handler.HedgingActionGenerator = args => () => Task.FromResult(0); + handler.ShouldHandle.HandleException(); + }) + .SetHedging(handler => + { + handler.HedgingActionGenerator = args => + { + args.Context.Should().NotBeNull(); + return () => Task.FromResult("dummy"); + }; + }) + .CreateHandler(); + + var args = new HandleHedgingArguments(ResilienceContext.Get()); + (await handler!.ShouldHandleAsync(new Outcome(new InvalidOperationException()), args)).Should().BeFalse(); + handler.HandlesHedging().Should().BeFalse(); + handler.TryCreateHedgedAction(ResilienceContext.Get()).Should().BeNull(); + } +} diff --git a/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyBuilderExtensionsTests.cs b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyBuilderExtensionsTests.cs new file mode 100644 index 00000000000..c4850f698ee --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyBuilderExtensionsTests.cs @@ -0,0 +1,54 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Polly.Hedging; + +namespace Polly.Core.Tests.Hedging; + +public class HedgingResilienceStrategyBuilderExtensionsTests +{ + private readonly ResilienceStrategyBuilder _builder = new(); + + public static readonly TheoryData> HedgingCases = new() + { + builder => + { + builder.AddHedging(new HedgingStrategyOptions()); + }, + builder => + { + builder.AddHedging(new HedgingStrategyOptions + { + HedgingActionGenerator = args => () => Task.FromResult(0) + }); + }, + }; + + [MemberData(nameof(HedgingCases))] + [Theory] + public void AddHedging_Ok(Action configure) + { + configure(_builder); + _builder.Build().Should().BeOfType(); + } + + [Fact] + public void AddHedging_InvalidOptions_Throws() + { + _builder + .Invoking(b => b.AddHedging(new HedgingStrategyOptions { Handler = null! })) + .Should() + .Throw() + .WithMessage("The hedging strategy options are invalid.*"); + } + + [Fact] + public void AddHedgingT_InvalidOptions_Throws() + { + _builder + .Invoking(b => b.AddHedging(new HedgingStrategyOptions { ShouldHandle = null! })) + .Should() + .Throw() + .WithMessage("The hedging strategy options are invalid.*"); + } +} diff --git a/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs new file mode 100644 index 00000000000..a636886457b --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -0,0 +1,59 @@ +using Polly.Hedging; + +namespace Polly.Core.Tests.Hedging; + +public class HedgingResilienceStrategyTests +{ + private readonly HedgingStrategyOptions _options = new(); + + [Fact] + public void Ctor_EnsureDefaults() + { + var strategy = Create(); + + strategy.MaxHedgedAttempts.Should().Be(_options.MaxHedgedAttempts); + strategy.HedgingDelay.Should().Be(_options.HedgingDelay); + strategy.HedgingDelayGenerator.Should().BeNull(); + strategy.HedgingHandler.Should().BeNull(); + strategy.HedgingHandler.Should().BeNull(); + } + + [Fact] + public void Execute_Skipped_Ok() + { + var strategy = Create(); + + strategy.Execute(_ => 10).Should().Be(10); + } + + [InlineData(-1)] + [InlineData(-1000)] + [InlineData(0)] + [InlineData(1)] + [InlineData(1000)] + [Theory] + public async Task GetHedgingDelayAsync_GeneratorSet_EnsureCorrectGeneratedValue(int seconds) + { + _options.HedgingDelayGenerator.SetGenerator(args => TimeSpan.FromSeconds(seconds)); + + var strategy = Create(); + + var result = await strategy.GetHedgingDelayAsync(ResilienceContext.Get(), 0); + + result.Should().Be(TimeSpan.FromSeconds(seconds)); + } + + [Fact] + public async Task GetHedgingDelayAsync_NoGeneratorSet_EnsureCorrectValue() + { + _options.HedgingDelay = TimeSpan.FromMilliseconds(123); + + var strategy = Create(); + + var result = await strategy.GetHedgingDelayAsync(ResilienceContext.Get(), 0); + + result.Should().Be(TimeSpan.FromMilliseconds(123)); + } + + private HedgingResilienceStrategy Create() => new(_options); +} diff --git a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs new file mode 100644 index 00000000000..000444175a2 --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs @@ -0,0 +1,98 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Hedging; +using Polly.Strategy; +using Polly.Utils; + +namespace Polly.Core.Tests.Hedging; + +public class HedgingStrategyOptionsTResultTests +{ + [Fact] + public void Ctor_EnsureDefaults() + { + var options = new HedgingStrategyOptions(); + + options.StrategyType.Should().Be("Hedging"); + options.ShouldHandle.Should().NotBeNull(); + options.ShouldHandle.IsEmpty.Should().BeTrue(); + options.HedgingActionGenerator.Should().BeNull(); + options.HedgingDelay.Should().Be(TimeSpan.FromSeconds(2)); + options.MaxHedgedAttempts.Should().Be(2); + options.OnHedging.IsEmpty.Should().BeTrue(); + } + + [Fact] + public void Validation() + { + var options = new HedgingStrategyOptions + { + HedgingDelayGenerator = null!, + ShouldHandle = null!, + MaxHedgedAttempts = -1, + OnHedging = null!, + }; + + options + .Invoking(o => ValidationHelper.ValidateObject(o, "Invalid.")) + .Should() + .Throw() + .WithMessage(""" + Invalid. + + Validation Errors: + The field MaxHedgedAttempts must be between 2 and 10. + The ShouldHandle field is required. + The HedgingActionGenerator field is required. + The HedgingDelayGenerator field is required. + The OnHedging field is required. + """); + } + + [Fact] + public async Task AsNonGenericOptions_Ok() + { + var onHedgingCalled = false; + var options = new HedgingStrategyOptions + { + HedgingDelayGenerator = new NoOutcomeGenerator().SetGenerator(args => TimeSpan.FromSeconds(123)), + ShouldHandle = new OutcomePredicate().HandleResult(-1), + StrategyName = "Dummy", + StrategyType = "Dummy-Hedging", + HedgingDelay = TimeSpan.FromSeconds(3), + MaxHedgedAttempts = 4, + HedgingActionGenerator = args => () => Task.FromResult(555), + OnHedging = new OutcomeEvent().Register((_, args) => + { + args.Context.Should().NotBeNull(); + args.Attempt.Should().Be(3); + onHedgingCalled = true; + }) + }; + + var nonGeneric = options.AsNonGenericOptions(); + + nonGeneric.Should().NotBeNull(); + nonGeneric.StrategyType.Should().Be("Dummy-Hedging"); + nonGeneric.StrategyName.Should().Be("Dummy"); + nonGeneric.Handler.IsEmpty.Should().BeFalse(); + nonGeneric.MaxHedgedAttempts.Should().Be(4); + nonGeneric.HedgingDelay.Should().Be(TimeSpan.FromSeconds(3)); + + var handler = nonGeneric.Handler.CreateHandler(); + handler.Should().NotBeNull(); + + (await handler!.TryCreateHedgedAction(ResilienceContext.Get())!()).Should().Be(555); + + var result = await handler!.ShouldHandleAsync(new Outcome(-1), new HandleHedgingArguments(ResilienceContext.Get())); + result.Should().BeTrue(); + + result = await handler!.ShouldHandleAsync(new Outcome(0), new HandleHedgingArguments(ResilienceContext.Get())); + result.Should().BeFalse(); + + var delay = await nonGeneric.HedgingDelayGenerator.CreateHandler(TimeSpan.Zero, _ => true)!(new HedgingDelayArguments(ResilienceContext.Get(), 4)); + delay.Should().Be(TimeSpan.FromSeconds(123)); + + await nonGeneric.OnHedging.CreateHandler()!.HandleAsync(new Outcome(10), new OnHedgingArguments(ResilienceContext.Get(), 3)); + onHedgingCalled.Should().BeTrue(); + } +} diff --git a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs new file mode 100644 index 00000000000..6f4bd5db42f --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Hedging; +using Polly.Utils; + +namespace Polly.Core.Tests.Hedging; + +public class HedgingStrategyOptionsTests +{ + [Fact] + public void Ctor_EnsureDefaults() + { + var options = new HedgingStrategyOptions(); + + HedgingStrategyOptions.InfiniteHedgingDelay.Should().Be(TimeSpan.FromMilliseconds(-1)); + options.StrategyType.Should().Be("Hedging"); + options.Handler.Should().NotBeNull(); + options.Handler.IsEmpty.Should().BeTrue(); + options.HedgingDelayGenerator.IsEmpty.Should().BeTrue(); + options.HedgingDelay.Should().Be(TimeSpan.FromSeconds(2)); + options.MaxHedgedAttempts.Should().Be(2); + options.OnHedging.IsEmpty.Should().BeTrue(); + } + + [Fact] + public void Validation() + { + var options = new HedgingStrategyOptions + { + HedgingDelayGenerator = null!, + Handler = null!, + MaxHedgedAttempts = -1, + OnHedging = null! + }; + + options + .Invoking(o => ValidationHelper.ValidateObject(o, "Invalid.")) + .Should() + .Throw() + .WithMessage(""" + Invalid. + + Validation Errors: + The field MaxHedgedAttempts must be between 2 and 10. + The Handler field is required. + The HedgingDelayGenerator field is required. + The OnHedging field is required. + """); + } +} diff --git a/src/Polly.Core/Fallback/FallbackHandler.Handler.cs b/src/Polly.Core/Fallback/FallbackHandler.Handler.cs index 7e63b81871f..e5e39a5f26d 100644 --- a/src/Polly.Core/Fallback/FallbackHandler.Handler.cs +++ b/src/Polly.Core/Fallback/FallbackHandler.Handler.cs @@ -2,7 +2,7 @@ namespace Polly.Fallback; -public partial class FallbackHandler +public sealed partial class FallbackHandler { internal sealed class Handler { diff --git a/src/Polly.Core/Fallback/FallbackHandler.TResult.cs b/src/Polly.Core/Fallback/FallbackHandler.TResult.cs index 4dbb25fdf9f..0f1327a439d 100644 --- a/src/Polly.Core/Fallback/FallbackHandler.TResult.cs +++ b/src/Polly.Core/Fallback/FallbackHandler.TResult.cs @@ -20,7 +20,7 @@ namespace Polly.Fallback; /// 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 +public sealed class FallbackHandler { /// /// Gets or sets the predicate that determines whether a fallback should be performed for a given result. diff --git a/src/Polly.Core/Fallback/FallbackHandler.cs b/src/Polly.Core/Fallback/FallbackHandler.cs index d9a5460a18f..3093a1200b9 100644 --- a/src/Polly.Core/Fallback/FallbackHandler.cs +++ b/src/Polly.Core/Fallback/FallbackHandler.cs @@ -5,7 +5,7 @@ namespace Polly.Fallback; /// /// Represents a class for managing fallback handlers. /// -public partial class FallbackHandler +public sealed partial class FallbackHandler { private readonly OutcomePredicate _predicates = new(); private readonly Dictionary _actions = new(); diff --git a/src/Polly.Core/Fallback/VoidFallbackHandler.cs b/src/Polly.Core/Fallback/VoidFallbackHandler.cs index 0594fffde20..b911ef29be1 100644 --- a/src/Polly.Core/Fallback/VoidFallbackHandler.cs +++ b/src/Polly.Core/Fallback/VoidFallbackHandler.cs @@ -18,7 +18,7 @@ namespace Polly.Fallback; /// 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 +public sealed class VoidFallbackHandler { /// /// Gets or sets the predicate that determines whether a fallback should be handled. diff --git a/src/Polly.Core/Hedging/HandleHedgingArguments.cs b/src/Polly.Core/Hedging/HandleHedgingArguments.cs new file mode 100644 index 00000000000..7ae945e80cd --- /dev/null +++ b/src/Polly.Core/Hedging/HandleHedgingArguments.cs @@ -0,0 +1,16 @@ +using Polly.Strategy; + +namespace Polly.Hedging; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Represents arguments used in hedging handling scenarios. +/// +public readonly struct HandleHedgingArguments : IResilienceArguments +{ + internal HandleHedgingArguments(ResilienceContext context) => Context = context; + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Hedging/HedgingActionGenerator.TResult.cs b/src/Polly.Core/Hedging/HedgingActionGenerator.TResult.cs new file mode 100644 index 00000000000..f0470b1ff06 --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingActionGenerator.TResult.cs @@ -0,0 +1,13 @@ +namespace Polly.Hedging; + +/// +/// Represents a generator that creates hedged actions. +/// +/// The result type. +/// The arguments passed to the generator. +/// A that represents an asynchronous operation. +/// +/// The generator can return a null function. In that case the hedging is not executed for that attempt. +/// Make sure that the returned action represents a real asynchronous work when invoked. +/// +public delegate Func>? HedgingActionGenerator(HedgingActionGeneratorArguments arguments); diff --git a/src/Polly.Core/Hedging/HedgingActionGenerator.cs b/src/Polly.Core/Hedging/HedgingActionGenerator.cs new file mode 100644 index 00000000000..4acfc480c7e --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingActionGenerator.cs @@ -0,0 +1,12 @@ +namespace Polly.Hedging; + +/// +/// Represents a generator that creates void-based hedged actions. +/// +/// The arguments passed to the generator. +/// A that represents an asynchronous operation. +/// +/// The generator can return a null function. In that case the hedging is not executed for that attempt. +/// Make sure that the returned action represents a real asynchronous work when invoked. +/// +public delegate Func? HedgingActionGenerator(HedgingActionGeneratorArguments arguments); diff --git a/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs b/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs new file mode 100644 index 00000000000..3cbbaca6b81 --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs @@ -0,0 +1,18 @@ +using Polly.Strategy; + +namespace Polly.Hedging; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Represents arguments used in . +/// +/// The type of the result. +public readonly struct HedgingActionGeneratorArguments : IResilienceArguments +{ + internal HedgingActionGeneratorArguments(ResilienceContext context) => Context = context; + + /// + public ResilienceContext Context { get; } +} + diff --git a/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.cs b/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.cs new file mode 100644 index 00000000000..cf1162f42ea --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.cs @@ -0,0 +1,16 @@ +using Polly.Strategy; + +namespace Polly.Hedging; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Represents arguments used in . +/// +public readonly struct HedgingActionGeneratorArguments : IResilienceArguments +{ + internal HedgingActionGeneratorArguments(ResilienceContext context) => Context = context; + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Hedging/HedgingConstants.cs b/src/Polly.Core/Hedging/HedgingConstants.cs new file mode 100644 index 00000000000..23a28fb401c --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingConstants.cs @@ -0,0 +1,14 @@ +namespace Polly.Hedging; + +internal static class HedgingConstants +{ + public const string StrategyType = "Hedging"; + + public const int DefaultMaxHedgedAttempts = 2; + + public const int MinimumHedgedAttempts = 2; + + public const int MaximumHedgedAttempts = 10; + + public static readonly TimeSpan DefaultHedgingDelay = TimeSpan.FromSeconds(2); +} diff --git a/src/Polly.Core/Hedging/HedgingDelayArguments.cs b/src/Polly.Core/Hedging/HedgingDelayArguments.cs new file mode 100644 index 00000000000..450384483fa --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingDelayArguments.cs @@ -0,0 +1,25 @@ +using Polly.Strategy; + +namespace Polly.Hedging; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by hedging delay generator. +/// +public readonly struct HedgingDelayArguments : IResilienceArguments +{ + internal HedgingDelayArguments(ResilienceContext context, int attempt) + { + Context = context; + Attempt = attempt; + } + + /// + /// Gets the zero-based hedging attempt number. + /// + public int Attempt { get; } + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Hedging/HedgingHandler.Handler.cs b/src/Polly.Core/Hedging/HedgingHandler.Handler.cs new file mode 100644 index 00000000000..56822bd7349 --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingHandler.Handler.cs @@ -0,0 +1,33 @@ +using Polly.Strategy; + +namespace Polly.Hedging; + +public partial class HedgingHandler +{ + internal sealed class Handler + { + private readonly OutcomePredicate.Handler _handler; + private readonly Dictionary _generators; + + internal Handler(OutcomePredicate.Handler handler, Dictionary generators) + { + _handler = handler; + _generators = generators; + } + + public bool HandlesHedging() => _generators.ContainsKey(typeof(TResult)); + + public ValueTask ShouldHandleAsync(Outcome outcome, HandleHedgingArguments arguments) => _handler.ShouldHandleAsync(outcome, arguments); + + public Func>? TryCreateHedgedAction(ResilienceContext context) + { + if (!_generators.TryGetValue(typeof(TResult), out var generator)) + { + return null; + } + + return ((HedgingActionGenerator)generator)(new HedgingActionGeneratorArguments(context)); + } + } +} + diff --git a/src/Polly.Core/Hedging/HedgingHandler.TResult.cs b/src/Polly.Core/Hedging/HedgingHandler.TResult.cs new file mode 100644 index 00000000000..8e161f408c0 --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingHandler.TResult.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.Hedging; + +/// +/// Encompasses logic for managing hedging operations for a single result type. +/// +/// The result type. +/// +/// Every hedging handler requires a predicate that determines whether a hedging should be performed for a given result and also +/// the hedging generator that creates a hedged action to execute. +/// +public sealed class HedgingHandler +{ + /// + /// Gets or sets the predicate that determines whether a hedging should be performed for a given result. + /// + [Required] + public OutcomePredicate ShouldHandle { get; set; } = new(); + + /// + /// Gets or sets the hedging action generator that creates hedged actions. + /// + /// + /// This property is required. Defaults to null. + /// + [Required] + public HedgingActionGenerator? HedgingActionGenerator { get; set; } = null; +} diff --git a/src/Polly.Core/Hedging/HedgingHandler.cs b/src/Polly.Core/Hedging/HedgingHandler.cs new file mode 100644 index 00000000000..87a6ebb0045 --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingHandler.cs @@ -0,0 +1,94 @@ +using Polly.Strategy; + +namespace Polly.Hedging; + +/// +/// Represents a class for managing hedging handlers. +/// +public sealed partial class HedgingHandler +{ + private readonly OutcomePredicate _predicates = new(); + private readonly Dictionary _actions = new(); + + /// + /// Gets a value indicating whether the hedging handler is empty. + /// + public bool IsEmpty => _predicates.IsEmpty; + + /// + /// Configures a hedging handler for a specific result type. + /// + /// The result type. + /// An action that configures the hedging handler instance for a specific result. + /// The current instance. + public HedgingHandler SetHedging(Action> configure) + { + Guard.NotNull(configure); + + var handler = new HedgingHandler(); + configure(handler); + + ValidationHelper.ValidateObject(handler, "The hedging handler configuration is invalid."); + + if (!handler.ShouldHandle.IsEmpty) + { + _predicates.SetPredicates(handler.ShouldHandle); + _actions[typeof(TResult)] = handler.HedgingActionGenerator!; + } + + return this; + } + + /// + /// Configures a void-based hedging handler. + /// + /// An action that configures the void-based hedging handler. + /// The current instance. + public HedgingHandler SetVoidHedging(Action configure) + { + Guard.NotNull(configure); + + var handler = new VoidHedgingHandler(); + configure(handler); + + ValidationHelper.ValidateObject(handler, "The hedging handler configuration is invalid."); + + if (!handler.ShouldHandle.IsEmpty) + { + _predicates.SetVoidPredicates(handler.ShouldHandle); + _actions[typeof(VoidResult)] = CreateGenericGenerator(handler.HedgingActionGenerator!); + } + + return this; + } + + internal Handler? CreateHandler() + { + var shouldHandle = _predicates.CreateHandler(); + if (shouldHandle == null) + { + return null; + } + + return new Handler(shouldHandle, _actions); + } + + private static HedgingActionGenerator CreateGenericGenerator(HedgingActionGenerator generator) + { + return (args) => + { + Func? action = generator(new HedgingActionGeneratorArguments(args.Context)); + if (action == null) + { + return null; + } + + return async () => + { + await action().ConfigureAwait(args.Context.ContinueOnCapturedContext); + return VoidResult.Instance; + }; + }; + } +} + diff --git a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs new file mode 100644 index 00000000000..291a0638bba --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs @@ -0,0 +1,38 @@ +using Polly.Hedging; + +namespace Polly; + +internal sealed class HedgingResilienceStrategy : ResilienceStrategy +{ + public HedgingResilienceStrategy(HedgingStrategyOptions options) + { + HedgingDelay = options.HedgingDelay; + MaxHedgedAttempts = options.MaxHedgedAttempts; + HedgingDelayGenerator = options.HedgingDelayGenerator.CreateHandler(HedgingConstants.DefaultHedgingDelay, static _ => true); + HedgingHandler = options.Handler.CreateHandler(); + } + + public TimeSpan HedgingDelay { get; } + + public int MaxHedgedAttempts { get; } + + public Func>? HedgingDelayGenerator { get; } + + public HedgingHandler.Handler? HedgingHandler { get; } + + protected internal override ValueTask ExecuteCoreAsync(Func> callback, ResilienceContext context, TState state) + { + return callback(context, state); + } + + internal ValueTask GetHedgingDelayAsync(ResilienceContext context, int attempt) + { + if (HedgingDelayGenerator == null) + { + return new ValueTask(HedgingDelay); + } + + return HedgingDelayGenerator(new HedgingDelayArguments(context, attempt)); + } + +} diff --git a/src/Polly.Core/Hedging/HedgingResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Hedging/HedgingResilienceStrategyBuilderExtensions.cs new file mode 100644 index 00000000000..a5565a2ab5f --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingResilienceStrategyBuilderExtensions.cs @@ -0,0 +1,42 @@ +using Polly.Hedging; + +namespace Polly; + +/// +/// Provides extension methods for configuring hedging resilience strategies for . +/// +public static class HedgingResilienceStrategyBuilderExtensions +{ + /// + /// Adds a hedging resilience strategy with the provided options to the builder. + /// + /// The result type. + /// The resilience strategy builder. + /// The options to configure the hedging resilience strategy. + /// The builder instance with the hedging strategy added. + public static ResilienceStrategyBuilder AddHedging(this ResilienceStrategyBuilder builder, HedgingStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The hedging strategy options are invalid."); + + return builder.AddHedging(options.AsNonGenericOptions()); + } + + /// + /// Adds a hedging resilience strategy with the provided options to the builder. + /// + /// The resilience strategy builder. + /// The options to configure the hedging resilience strategy. + /// The builder instance with the hedging strategy added. + public static ResilienceStrategyBuilder AddHedging(this ResilienceStrategyBuilder builder, HedgingStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The hedging strategy options are invalid."); + + return builder.AddStrategy(context => new HedgingResilienceStrategy(options), options); + } +} diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs new file mode 100644 index 00000000000..2bf8bd023cd --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs @@ -0,0 +1,92 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.Hedging; + +/// +/// Hedging strategy options. +/// +/// The type of result these hedging options handle. +public class HedgingStrategyOptions : ResilienceStrategyOptions +{ + /// + /// Initializes a new instance of the class. + /// + public HedgingStrategyOptions() => StrategyType = HedgingConstants.StrategyType; + + /// + /// Gets or sets the minimal time of waiting before spawning a new hedged call. + /// + /// + /// Defaults to 2 seconds. + /// + /// You can also use to create all hedged tasks (value of ) at once + /// or to force the hedging strategy to never create new task before the old one is finished. + /// + /// If you want a greater control over hedging delay customization use . + /// + public TimeSpan HedgingDelay { get; set; } = HedgingConstants.DefaultHedgingDelay; + + /// + /// Gets or sets the maximum hedged attempts to perform the desired task. + /// + /// + /// Defaults to 2. The value must be bigger or equal to 2, and lower or equal to 10. + /// + /// The value defines how many concurrent hedged tasks will be triggered by the strategy. + /// This includes the primary hedged task that is initially performed, and the further tasks that will + /// be fetched from the provider and spawned in parallel. + /// + /// + [Range(HedgingConstants.MinimumHedgedAttempts, HedgingConstants.MaximumHedgedAttempts)] + public int MaxHedgedAttempts { get; set; } = HedgingConstants.DefaultMaxHedgedAttempts; + + /// + /// Gets or sets the predicate that determines whether a hedging should be performed for a given result. + /// + [Required] + public OutcomePredicate ShouldHandle { get; set; } = new(); + + /// + /// Gets or sets the hedging action generator that creates hedged actions. + /// + [Required] + public HedgingActionGenerator? HedgingActionGenerator { get; set; } = null; + + /// + /// Gets or sets the generator that generates hedging delays for each hedging attempt. + /// + /// + /// The takes precedence over . If specified, the is ignored. + /// By default, this generator is empty and does not have any custom hedging delays. + /// + [Required] + public NoOutcomeGenerator HedgingDelayGenerator { get; set; } = new(); + + /// + /// Gets or sets the event that is triggered when a hedging is performed. + /// + /// + /// This property is required. By default, this event is empty. + /// + [Required] + public OutcomeEvent OnHedging { get; set; } = new(); + + internal HedgingStrategyOptions AsNonGenericOptions() + { + return new HedgingStrategyOptions + { + StrategyType = StrategyType, + StrategyName = StrategyName, + HedgingDelay = HedgingDelay, + HedgingDelayGenerator = HedgingDelayGenerator, + Handler = new HedgingHandler().SetHedging(handler => + { + handler.ShouldHandle = ShouldHandle; + handler.HedgingActionGenerator = HedgingActionGenerator; + }), + MaxHedgedAttempts = MaxHedgedAttempts, + OnHedging = new OutcomeEvent().SetCallbacks(OnHedging) + }; + } +} diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs new file mode 100644 index 00000000000..ae0cfe7eb60 --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs @@ -0,0 +1,74 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.Hedging; + +/// +/// Hedging strategy options. +/// +public class HedgingStrategyOptions : ResilienceStrategyOptions +{ + /// + /// Initializes a new instance of the class. + /// + public HedgingStrategyOptions() => StrategyType = HedgingConstants.StrategyType; + + /// + /// A that represents the infinite hedging delay. + /// + public static readonly TimeSpan InfiniteHedgingDelay = System.Threading.Timeout.InfiniteTimeSpan; + + /// + /// Gets or sets the minimal time of waiting before spawning a new hedged call. + /// + /// + /// Defaults to 2 seconds. + /// + /// You can also use to create all hedged tasks (value of ) at once + /// or to force the hedging strategy to never create new task before the old one is finished. + /// + /// + /// If you want a greater control over hedging delay customization use . + /// + /// + public TimeSpan HedgingDelay { get; set; } = HedgingConstants.DefaultHedgingDelay; + + /// + /// Gets or sets the maximum hedged attempts to perform the desired task. + /// + /// + /// Defaults to 2. The value must be bigger or equal to 2, and lower or equal to 10. + /// + /// The value defines how many concurrent hedged tasks will be triggered by the strategy. + /// This includes the primary hedged task that is initially performed, and the further tasks that will + /// be fetched from the provider and spawned in parallel. + /// + /// + [Range(HedgingConstants.MinimumHedgedAttempts, HedgingConstants.MaximumHedgedAttempts)] + public int MaxHedgedAttempts { get; set; } = HedgingConstants.DefaultMaxHedgedAttempts; + + /// + /// Gets or sets the hedging handler that manages hedging operations for multiple result types. + /// + [Required] + public HedgingHandler Handler { get; set; } = new(); + + /// + /// Gets or sets the generator that generates hedging delays for each hedging attempt. + /// + /// + /// The takes precedence over . If specified, the is ignored. + /// By default, this generator is empty and does not have any custom hedging delays. + /// + [Required] + public NoOutcomeGenerator HedgingDelayGenerator { get; set; } = new(); + + /// + /// Gets or sets the event that is triggered when a hedging is performed. + /// + /// + /// This property is required. By default, this event is empty. + /// + [Required] + public OutcomeEvent OnHedging { get; set; } = new(); +} diff --git a/src/Polly.Core/Hedging/OnHedgingArguments.cs b/src/Polly.Core/Hedging/OnHedgingArguments.cs new file mode 100644 index 00000000000..c544c5e6508 --- /dev/null +++ b/src/Polly.Core/Hedging/OnHedgingArguments.cs @@ -0,0 +1,25 @@ +using Polly.Strategy; + +namespace Polly.Hedging; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Represents arguments used by the on-hedging event. +/// +public readonly struct OnHedgingArguments : IResilienceArguments +{ + internal OnHedgingArguments(ResilienceContext context, int attempt) + { + Context = context; + Attempt = attempt; + } + + /// + public ResilienceContext Context { get; } + + /// + /// Gets the zero-based hedging attempt number. + /// + public int Attempt { get; } +} diff --git a/src/Polly.Core/Hedging/VoidHedgingHandler.cs b/src/Polly.Core/Hedging/VoidHedgingHandler.cs new file mode 100644 index 00000000000..6212e715b69 --- /dev/null +++ b/src/Polly.Core/Hedging/VoidHedgingHandler.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Strategy; + +namespace Polly.Hedging; + +/// +/// Encompasses logic for managing hedging operations for a void-based result type. +/// +/// +/// Every hedging handler requires a predicate that determines whether a hedging should be performed for a given void result and also +/// the hedging generator that creates a hedged action to execute. +/// +public sealed class VoidHedgingHandler +{ + /// + /// Gets or sets the predicate that determines whether a hedging should be performed for a given void-based result. + /// + [Required] + public VoidOutcomePredicate ShouldHandle { get; set; } = new(); + + /// + /// Gets or sets the hedging action generator that creates hedged actions. + /// + /// + /// This property is required. Defaults to null. + /// + [Required] + public HedgingActionGenerator? HedgingActionGenerator { get; set; } = null; +} diff --git a/src/Polly.Core/Strategy/NoOutcomeGenerator.cs b/src/Polly.Core/Strategy/NoOutcomeGenerator.cs index 7a51fc9b2ec..fbd0f2fae56 100644 --- a/src/Polly.Core/Strategy/NoOutcomeGenerator.cs +++ b/src/Polly.Core/Strategy/NoOutcomeGenerator.cs @@ -10,6 +10,11 @@ public sealed class NoOutcomeGenerator { private Func>? _generator; + /// + /// Gets a value indicating whether the generator is empty. + /// + public bool IsEmpty => _generator is null; + /// /// Adds a result generator. ///