From a8b977e13261c748526549840a1f1b4d7d4c7531 Mon Sep 17 00:00:00 2001 From: martintmk Date: Tue, 25 Apr 2023 23:32:05 +0200 Subject: [PATCH 1/4] Introduce public API for Hedging Resilience Strategy --- .../Hedging/HandleHedgingArgumentsTests.cs | 14 ++ .../Hedging/HedgingDelayArgumentsTests.cs | 15 ++ .../Hedging/HedgingHandlerTests.cs | 160 ++++++++++++++++++ ...esilienceStrategyBuilderExtensionsTests.cs | 54 ++++++ .../Hedging/HedgingResilienceStrategyTests.cs | 58 +++++++ .../HedgingStrategyOptionsTResultTests.cs | 85 ++++++++++ .../Hedging/HedgingStrategyOptionsTests.cs | 46 +++++ .../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 | 98 +++++++++++ .../Hedging/HedgingResilienceStrategy.cs | 38 +++++ ...gingResilienceStrategyBuilderExtensions.cs | 42 +++++ .../Hedging/HedgingStrategyOptions.TResult.cs | 82 +++++++++ .../Hedging/HedgingStrategyOptions.cs | 63 +++++++ src/Polly.Core/Hedging/VoidHedgingHandler.cs | 29 ++++ src/Polly.Core/Strategy/NoOutcomeGenerator.cs | 5 + 27 files changed, 970 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/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..0e588c45399 --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -0,0 +1,58 @@ +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(); + } + + [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..0b76f307457 --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs @@ -0,0 +1,85 @@ +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); + } + + [Fact] + public void Validation() + { + var options = new HedgingStrategyOptions + { + HedgingDelayGenerator = null!, + ShouldHandle = null!, + MaxHedgedAttempts = -1, + }; + + 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. + """); + } + + [Fact] + public async Task AsNonGenericOptions_Ok() + { + 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) + }; + + 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)); + } +} diff --git a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs new file mode 100644 index 00000000000..e767a90de38 --- /dev/null +++ b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs @@ -0,0 +1,46 @@ +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); + } + + [Fact] + public void Validation() + { + var options = new HedgingStrategyOptions + { + HedgingDelayGenerator = null!, + Handler = null!, + MaxHedgedAttempts = -1, + }; + + 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. + """); + } +} 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..1d4e6378a3d --- /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 represent 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..bc2edbac090 --- /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 represent 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..5865095d27a --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingHandler.cs @@ -0,0 +1,98 @@ +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) + { + return this; + } + + _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) + { + return this; + } + + _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..999d9dd4bf0 --- /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, _ => 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..907b8991e94 --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs @@ -0,0 +1,82 @@ +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. + /// + /// + /// Default is set 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. + /// + /// + /// Default set to 2. + /// 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. + /// The value must be bigger or equal to 2, and lower or equal to 10. + /// + [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 on hedging delays. + /// + [Required] + public NoOutcomeGenerator HedgingDelayGenerator { 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 + }; + } +} diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs new file mode 100644 index 00000000000..9908357b295 --- /dev/null +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs @@ -0,0 +1,63 @@ +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 = TimeSpan.FromMilliseconds(-1); + + /// + /// Gets or sets the minimal time of waiting before spawning a new hedged call. + /// + /// + /// Default is set 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. + /// + /// + /// Default set to 2. + /// 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. + /// The value must be bigger or equal to 2, and lower or equal to 10. + /// + [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 on hedging delays. + /// + [Required] + public NoOutcomeGenerator HedgingDelayGenerator { get; set; } = new(); +} 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..e09152d359d 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 == null; + /// /// Adds a result generator. /// From f7623ae0336db0db11f9409dc40eabdcce1c63ce Mon Sep 17 00:00:00 2001 From: martintmk Date: Wed, 26 Apr 2023 10:56:24 +0200 Subject: [PATCH 2/4] Add mising OnHedging event --- .../Hedging/HedgingResilienceStrategyTests.cs | 1 + .../HedgingStrategyOptionsTResultTests.cs | 15 ++++++++++- .../Hedging/HedgingStrategyOptionsTests.cs | 3 +++ .../Hedging/HedgingStrategyOptions.TResult.cs | 12 ++++++++- .../Hedging/HedgingStrategyOptions.cs | 9 +++++++ src/Polly.Core/Hedging/OnHedgingArguments.cs | 25 +++++++++++++++++++ 6 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/Polly.Core/Hedging/OnHedgingArguments.cs diff --git a/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index 0e588c45399..a636886457b 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -15,6 +15,7 @@ public void Ctor_EnsureDefaults() strategy.HedgingDelay.Should().Be(_options.HedgingDelay); strategy.HedgingDelayGenerator.Should().BeNull(); strategy.HedgingHandler.Should().BeNull(); + strategy.HedgingHandler.Should().BeNull(); } [Fact] diff --git a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs index 0b76f307457..000444175a2 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs @@ -18,6 +18,7 @@ public void Ctor_EnsureDefaults() options.HedgingActionGenerator.Should().BeNull(); options.HedgingDelay.Should().Be(TimeSpan.FromSeconds(2)); options.MaxHedgedAttempts.Should().Be(2); + options.OnHedging.IsEmpty.Should().BeTrue(); } [Fact] @@ -28,6 +29,7 @@ public void Validation() HedgingDelayGenerator = null!, ShouldHandle = null!, MaxHedgedAttempts = -1, + OnHedging = null!, }; options @@ -42,12 +44,14 @@ 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)), @@ -56,7 +60,13 @@ public async Task AsNonGenericOptions_Ok() StrategyType = "Dummy-Hedging", HedgingDelay = TimeSpan.FromSeconds(3), MaxHedgedAttempts = 4, - HedgingActionGenerator = args => () => Task.FromResult(555) + 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(); @@ -81,5 +91,8 @@ public async Task AsNonGenericOptions_Ok() 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 index e767a90de38..6f4bd5db42f 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs @@ -18,6 +18,7 @@ public void Ctor_EnsureDefaults() options.HedgingDelayGenerator.IsEmpty.Should().BeTrue(); options.HedgingDelay.Should().Be(TimeSpan.FromSeconds(2)); options.MaxHedgedAttempts.Should().Be(2); + options.OnHedging.IsEmpty.Should().BeTrue(); } [Fact] @@ -28,6 +29,7 @@ public void Validation() HedgingDelayGenerator = null!, Handler = null!, MaxHedgedAttempts = -1, + OnHedging = null! }; options @@ -41,6 +43,7 @@ public void Validation() 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/Hedging/HedgingStrategyOptions.TResult.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs index 907b8991e94..f0de050975e 100644 --- a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs @@ -63,6 +63,15 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions [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 @@ -76,7 +85,8 @@ internal HedgingStrategyOptions AsNonGenericOptions() handler.ShouldHandle = ShouldHandle; handler.HedgingActionGenerator = HedgingActionGenerator; }), - MaxHedgedAttempts = MaxHedgedAttempts + MaxHedgedAttempts = MaxHedgedAttempts, + OnHedging = new OutcomeEvent().SetCallbacks(OnHedging) }; } } diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs index 9908357b295..f3aaacd3e99 100644 --- a/src/Polly.Core/Hedging/HedgingStrategyOptions.cs +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs @@ -60,4 +60,13 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// [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..b30a640119c --- /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 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; } +} From 8f04b7497d609cce2f59d67bb64061133e4f37a7 Mon Sep 17 00:00:00 2001 From: martintmk Date: Wed, 26 Apr 2023 12:11:00 +0200 Subject: [PATCH 3/4] PR comments --- .../Hedging/HedgingActionGenerator.TResult.cs | 2 +- src/Polly.Core/Hedging/HedgingActionGenerator.cs | 2 +- src/Polly.Core/Hedging/HedgingHandler.cs | 16 ++++++---------- .../Hedging/HedgingResilienceStrategy.cs | 2 +- .../Hedging/HedgingStrategyOptions.TResult.cs | 9 ++++----- src/Polly.Core/Hedging/HedgingStrategyOptions.cs | 11 ++++++----- src/Polly.Core/Hedging/OnHedgingArguments.cs | 2 +- src/Polly.Core/Strategy/NoOutcomeGenerator.cs | 2 +- 8 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/Polly.Core/Hedging/HedgingActionGenerator.TResult.cs b/src/Polly.Core/Hedging/HedgingActionGenerator.TResult.cs index 1d4e6378a3d..f0470b1ff06 100644 --- a/src/Polly.Core/Hedging/HedgingActionGenerator.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingActionGenerator.TResult.cs @@ -8,6 +8,6 @@ namespace Polly.Hedging; /// 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 represent a real asynchronous work when invoked. +/// 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 index bc2edbac090..4acfc480c7e 100644 --- a/src/Polly.Core/Hedging/HedgingActionGenerator.cs +++ b/src/Polly.Core/Hedging/HedgingActionGenerator.cs @@ -7,6 +7,6 @@ namespace Polly.Hedging; /// 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 represent a real asynchronous work when invoked. +/// 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/HedgingHandler.cs b/src/Polly.Core/Hedging/HedgingHandler.cs index 5865095d27a..87a6ebb0045 100644 --- a/src/Polly.Core/Hedging/HedgingHandler.cs +++ b/src/Polly.Core/Hedging/HedgingHandler.cs @@ -30,14 +30,12 @@ public HedgingHandler SetHedging(Action> config ValidationHelper.ValidateObject(handler, "The hedging handler configuration is invalid."); - if (handler.ShouldHandle.IsEmpty) + if (!handler.ShouldHandle.IsEmpty) { - return this; + _predicates.SetPredicates(handler.ShouldHandle); + _actions[typeof(TResult)] = handler.HedgingActionGenerator!; } - _predicates.SetPredicates(handler.ShouldHandle); - _actions[typeof(TResult)] = handler.HedgingActionGenerator!; - return this; } @@ -55,14 +53,12 @@ public HedgingHandler SetVoidHedging(Action configure) ValidationHelper.ValidateObject(handler, "The hedging handler configuration is invalid."); - if (handler.ShouldHandle.IsEmpty) + if (!handler.ShouldHandle.IsEmpty) { - return this; + _predicates.SetVoidPredicates(handler.ShouldHandle); + _actions[typeof(VoidResult)] = CreateGenericGenerator(handler.HedgingActionGenerator!); } - _predicates.SetVoidPredicates(handler.ShouldHandle); - _actions[typeof(VoidResult)] = CreateGenericGenerator(handler.HedgingActionGenerator!); - return this; } diff --git a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs index 999d9dd4bf0..291a0638bba 100644 --- a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs +++ b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs @@ -8,7 +8,7 @@ public HedgingResilienceStrategy(HedgingStrategyOptions options) { HedgingDelay = options.HedgingDelay; MaxHedgedAttempts = options.MaxHedgedAttempts; - HedgingDelayGenerator = options.HedgingDelayGenerator.CreateHandler(HedgingConstants.DefaultHedgingDelay, _ => true); + HedgingDelayGenerator = options.HedgingDelayGenerator.CreateHandler(HedgingConstants.DefaultHedgingDelay, static _ => true); HedgingHandler = options.Handler.CreateHandler(); } diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs index f0de050975e..34f101d868f 100644 --- a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs @@ -19,11 +19,11 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// /// /// Default is set 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 . + /// + /// If you want a greater control over hedging delay customization use . /// public TimeSpan HedgingDelay { get; set; } = HedgingConstants.DefaultHedgingDelay; @@ -57,8 +57,7 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// /// /// The takes precedence over . If specified, the is ignored. - /// - /// By default, this generator is empty and does not have on hedging delays. + /// By default, this generator is empty and does not have any custom hedging delays. /// [Required] public NoOutcomeGenerator HedgingDelayGenerator { get; set; } = new(); diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs index f3aaacd3e99..0064f8e6d4e 100644 --- a/src/Polly.Core/Hedging/HedgingStrategyOptions.cs +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs @@ -16,18 +16,20 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// /// A that represents the infinite hedging delay. /// - public static readonly TimeSpan InfiniteHedgingDelay = TimeSpan.FromMilliseconds(-1); + public static readonly TimeSpan InfiniteHedgingDelay = System.Threading.Timeout.InfiniteTimeSpan; /// /// Gets or sets the minimal time of waiting before spawning a new hedged call. /// /// /// Default is set 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; @@ -55,8 +57,7 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// /// /// The takes precedence over . If specified, the is ignored. - /// - /// By default, this generator is empty and does not have on hedging delays. + /// By default, this generator is empty and does not have any custom hedging delays. /// [Required] public NoOutcomeGenerator HedgingDelayGenerator { get; set; } = new(); diff --git a/src/Polly.Core/Hedging/OnHedgingArguments.cs b/src/Polly.Core/Hedging/OnHedgingArguments.cs index b30a640119c..c544c5e6508 100644 --- a/src/Polly.Core/Hedging/OnHedgingArguments.cs +++ b/src/Polly.Core/Hedging/OnHedgingArguments.cs @@ -5,7 +5,7 @@ namespace Polly.Hedging; #pragma warning disable CA1815 // Override equals and operator equals on value types /// -/// Represents arguments used by on-hedging event. +/// Represents arguments used by the on-hedging event. /// public readonly struct OnHedgingArguments : IResilienceArguments { diff --git a/src/Polly.Core/Strategy/NoOutcomeGenerator.cs b/src/Polly.Core/Strategy/NoOutcomeGenerator.cs index e09152d359d..fbd0f2fae56 100644 --- a/src/Polly.Core/Strategy/NoOutcomeGenerator.cs +++ b/src/Polly.Core/Strategy/NoOutcomeGenerator.cs @@ -13,7 +13,7 @@ public sealed class NoOutcomeGenerator /// /// Gets a value indicating whether the generator is empty. /// - public bool IsEmpty => _generator == null; + public bool IsEmpty => _generator is null; /// /// Adds a result generator. From 38b7f3712d13482e922d2b2f80dbf200c41ce399 Mon Sep 17 00:00:00 2001 From: martintmk Date: Wed, 26 Apr 2023 12:22:15 +0200 Subject: [PATCH 4/4] cleanup --- src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs | 7 ++++--- src/Polly.Core/Hedging/HedgingStrategyOptions.cs | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs index 34f101d868f..2bf8bd023cd 100644 --- a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs @@ -18,7 +18,7 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// Gets or sets the minimal time of waiting before spawning a new hedged call. /// /// - /// Default is set to 2 seconds. + /// 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. @@ -31,11 +31,12 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// Gets or sets the maximum hedged attempts to perform the desired task. /// /// - /// Default set to 2. + /// 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. - /// The value must be bigger or equal to 2, and lower or equal to 10. + /// /// [Range(HedgingConstants.MinimumHedgedAttempts, HedgingConstants.MaximumHedgedAttempts)] public int MaxHedgedAttempts { get; set; } = HedgingConstants.DefaultMaxHedgedAttempts; diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs index 0064f8e6d4e..ae0cfe7eb60 100644 --- a/src/Polly.Core/Hedging/HedgingStrategyOptions.cs +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.cs @@ -22,7 +22,7 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// Gets or sets the minimal time of waiting before spawning a new hedged call. /// /// - /// Default is set to 2 seconds. + /// 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. @@ -37,11 +37,12 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// Gets or sets the maximum hedged attempts to perform the desired task. /// /// - /// Default set to 2. + /// 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. - /// The value must be bigger or equal to 2, and lower or equal to 10. + /// /// [Range(HedgingConstants.MinimumHedgedAttempts, HedgingConstants.MaximumHedgedAttempts)] public int MaxHedgedAttempts { get; set; } = HedgingConstants.DefaultMaxHedgedAttempts;