From 222958e206844f6dc3d86537d0f13cd408633856 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Thu, 21 Sep 2023 13:27:59 +0200 Subject: [PATCH] Introduce MaxDelay property to RetryStrategyOptions --- src/Polly.Core/PublicAPI.Unshipped.txt | 2 + src/Polly.Core/Retry/RetryHelper.cs | 45 ++++- .../Retry/RetryResilienceStrategy.cs | 5 +- .../Retry/RetryStrategyOptions.TResult.cs | 18 +- .../Retry/RetryHelperTests.cs | 164 +++++++++++++----- .../Retry/RetryResilienceStrategyTests.cs | 24 +++ .../Retry/RetryStrategyOptionsTests.cs | 4 +- 7 files changed, 210 insertions(+), 52 deletions(-) diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index 74c19f8c4ed..96906d8113d 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -309,6 +309,8 @@ Polly.Retry.RetryStrategyOptions.Delay.get -> System.TimeSpan Polly.Retry.RetryStrategyOptions.Delay.set -> void Polly.Retry.RetryStrategyOptions.DelayGenerator.get -> System.Func, System.Threading.Tasks.ValueTask>? Polly.Retry.RetryStrategyOptions.DelayGenerator.set -> void +Polly.Retry.RetryStrategyOptions.MaxDelay.get -> System.TimeSpan? +Polly.Retry.RetryStrategyOptions.MaxDelay.set -> void Polly.Retry.RetryStrategyOptions.MaxRetryAttempts.get -> int Polly.Retry.RetryStrategyOptions.MaxRetryAttempts.set -> void Polly.Retry.RetryStrategyOptions.OnRetry.get -> System.Func, System.Threading.Tasks.ValueTask>? diff --git a/src/Polly.Core/Retry/RetryHelper.cs b/src/Polly.Core/Retry/RetryHelper.cs index c2afdcf13bd..12630e2ecba 100644 --- a/src/Polly.Core/Retry/RetryHelper.cs +++ b/src/Polly.Core/Retry/RetryHelper.cs @@ -4,15 +4,38 @@ internal static class RetryHelper { private const double JitterFactor = 0.5; + private const double MaxDelayJitterFactor = JitterFactor / 2.0; + private const double ExponentialFactor = 2.0; public static bool IsValidDelay(TimeSpan delay) => delay >= TimeSpan.Zero; - public static TimeSpan GetRetryDelay(DelayBackoffType type, bool jitter, int attempt, TimeSpan baseDelay, ref double state, Func randomizer) + public static TimeSpan GetRetryDelay( + DelayBackoffType type, + bool jitter, + int attempt, + TimeSpan baseDelay, + TimeSpan? maxDelay, + ref double state, + Func randomizer) { try { - return GetRetryDelayCore(type, jitter, attempt, baseDelay, ref state, randomizer); + var delay = GetRetryDelayCore(type, jitter, attempt, baseDelay, ref state, randomizer); + + // stryker disable once equality : no means to test this + if (maxDelay is TimeSpan maxDelayValue && delay > maxDelayValue) + { + if (jitter) + { + // Apply jitter also to max delay, we are only allowed to substract from the delay not add to it. + return ApplyJitterForMaxDelay(maxDelayValue, randomizer); + } + + return maxDelay.Value; + } + + return delay; } catch (OverflowException) { @@ -89,7 +112,7 @@ private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDe long targetTicksFirstDelay = baseDelay.Ticks; double t = attempt + randomizer(); - double next = Math.Pow(2, t) * Math.Tanh(Math.Sqrt(PFactor * t)); + double next = Math.Pow(ExponentialFactor, t) * Math.Tanh(Math.Sqrt(PFactor * t)); double formulaIntrinsicValue = next - prev; prev = next; @@ -98,5 +121,19 @@ private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDe } private static TimeSpan ApplyJitter(TimeSpan delay, Func randomizer) - => TimeSpan.FromMilliseconds(delay.TotalMilliseconds + ((delay.TotalMilliseconds * JitterFactor) * randomizer())); + { + var offset = (delay.TotalMilliseconds * JitterFactor) / 2; + var randomDelay = (delay.TotalMilliseconds * JitterFactor * randomizer()) - offset; + var newDelay = delay.TotalMilliseconds + randomDelay; + + return TimeSpan.FromMilliseconds(newDelay); + } + + private static TimeSpan ApplyJitterForMaxDelay(TimeSpan delay, Func randomizer) + { + var randomDelay = delay.TotalMilliseconds * MaxDelayJitterFactor * randomizer(); + var newDelay = delay.TotalMilliseconds - randomDelay; + + return TimeSpan.FromMilliseconds(newDelay); + } } diff --git a/src/Polly.Core/Retry/RetryResilienceStrategy.cs b/src/Polly.Core/Retry/RetryResilienceStrategy.cs index 9eb5f1068c0..0b9dfbacde1 100644 --- a/src/Polly.Core/Retry/RetryResilienceStrategy.cs +++ b/src/Polly.Core/Retry/RetryResilienceStrategy.cs @@ -15,6 +15,7 @@ public RetryResilienceStrategy( { ShouldHandle = options.ShouldHandle; BaseDelay = options.Delay; + MaxDelay = options.MaxDelay; BackoffType = options.BackoffType; RetryCount = options.MaxRetryAttempts; OnRetry = options.OnRetry; @@ -28,6 +29,8 @@ public RetryResilienceStrategy( public TimeSpan BaseDelay { get; } + public TimeSpan? MaxDelay { get; } + public DelayBackoffType BackoffType { get; } public int RetryCount { get; } @@ -61,7 +64,7 @@ protected internal override async ValueTask> ExecuteCore(Func return outcome; } - var delay = RetryHelper.GetRetryDelay(BackoffType, UseJitter, attempt, BaseDelay, ref retryState, _randomizer); + var delay = RetryHelper.GetRetryDelay(BackoffType, UseJitter, attempt, BaseDelay, MaxDelay, ref retryState, _randomizer); if (DelayGenerator is not null) { var delayArgs = new RetryDelayGeneratorArguments(context, outcome, attempt); diff --git a/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs b/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs index fa568480694..70375d07f6c 100644 --- a/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs +++ b/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs @@ -3,6 +3,8 @@ namespace Polly.Retry; +#pragma warning disable IL2026 // Addressed with DynamicDependency on ValidationHelper.Validate method + /// /// Represents the options used to configure a retry strategy. /// @@ -49,7 +51,6 @@ public class RetryStrategyOptions : ResilienceStrategyOptions /// public bool UseJitter { get; set; } -#pragma warning disable IL2026 // Addressed with DynamicDependency on ValidationHelper.Validate method /// /// Gets or sets the base delay between retries. /// @@ -73,7 +74,20 @@ public class RetryStrategyOptions : ResilienceStrategyOptions /// [Range(typeof(TimeSpan), "00:00:00", "1.00:00:00")] public TimeSpan Delay { get; set; } = RetryConstants.DefaultBaseDelay; -#pragma warning restore IL2026 + + /// + /// Gets or sets the maximum delay between retries. + /// + /// + /// This property is used to cap maximum delay between retries. It is useful when you want to limit the maximum delay after certain + /// number of between retries when it could reach a unreasonably high values, especially if backoff is used. + /// If not specified, the delay is not capped. This property is ignored for delays generated by . + /// + /// + /// The default value is . + /// + [Range(typeof(TimeSpan), "00:00:00", "1.00:00:00")] + public TimeSpan? MaxDelay { get; set; } /// /// Gets or sets a predicate that determines whether the retry should be executed for a given outcome. diff --git a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs index f5d78eac08e..8bcdc2f9a17 100644 --- a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs @@ -28,57 +28,108 @@ public void UnsupportedRetryBackoffType_Throws(bool jitter) Assert.Throws(() => { double state = 0; - return RetryHelper.GetRetryDelay(type, jitter, 0, TimeSpan.FromSeconds(1), ref state, _randomizer); + return RetryHelper.GetRetryDelay(type, jitter, 0, TimeSpan.FromSeconds(1), null, ref state, _randomizer); }); } - [InlineData(true)] - [InlineData(false)] - [Theory] - public void Constant_Ok(bool jitter) + [Fact] + public void Constant_Ok() { double state = 0; - if (jitter) - { - _randomizer = () => 0.5; - } - RetryHelper.GetRetryDelay(DelayBackoffType.Constant, jitter, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); - RetryHelper.GetRetryDelay(DelayBackoffType.Constant, jitter, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); - RetryHelper.GetRetryDelay(DelayBackoffType.Constant, jitter, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, false, 0, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, false, 1, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, false, 2, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, false, 0, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1)); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, false, 1, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1)); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, false, 2, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1)); + } - var expected = !jitter ? TimeSpan.FromSeconds(1) : TimeSpan.FromSeconds(1.25); + [Fact] + public void Constant_Jitter_Ok() + { + double state = 0; - RetryHelper.GetRetryDelay(DelayBackoffType.Constant, jitter, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(expected); - RetryHelper.GetRetryDelay(DelayBackoffType.Constant, jitter, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(expected); - RetryHelper.GetRetryDelay(DelayBackoffType.Constant, jitter, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(expected); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, true, 0, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, true, 1, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, true, 2, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + + _randomizer = () => 0.0; + RetryHelper + .GetRetryDelay(DelayBackoffType.Constant, true, 0, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(0.75)); + + _randomizer = () => 0.4; + RetryHelper + .GetRetryDelay(DelayBackoffType.Constant, true, 0, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(0.95)); + + _randomizer = () => 0.6; + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, true, 1, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(1.05)); + + _randomizer = () => 1.0; + RetryHelper + .GetRetryDelay(DelayBackoffType.Constant, true, 0, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(1.25)); } - [InlineData(true)] - [InlineData(false)] - [Theory] - public void Linear_Ok(bool jitter) + [Fact] + public void Linear_Ok() { double state = 0; - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, false, 0, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, false, 1, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, false, 2, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); - if (jitter) - { - _randomizer = () => 0.5; + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, false, 0, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1)); + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, false, 1, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2)); + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, false, 2, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(3)); + } - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1.25)); - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2.5)); - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(3.75)); - } - else - { - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1)); - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2)); - RetryHelper.GetRetryDelay(DelayBackoffType.Linear, jitter, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(3)); - } + [Fact] + public void Linear_Jitter_Ok() + { + double state = 0; + + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, true, 0, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, true, 1, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, true, 2, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + + _randomizer = () => 0.0; + RetryHelper + .GetRetryDelay(DelayBackoffType.Linear, true, 2, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(2.25)); + + _randomizer = () => 0.4; + RetryHelper + .GetRetryDelay(DelayBackoffType.Linear, true, 2, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(2.85)); + + _randomizer = () => 0.5; + RetryHelper + .GetRetryDelay(DelayBackoffType.Linear, true, 2, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(3)); + + _randomizer = () => 0.6; + RetryHelper.GetRetryDelay(DelayBackoffType.Linear, true, 2, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(3.15)); + + _randomizer = () => 1.0; + RetryHelper + .GetRetryDelay(DelayBackoffType.Linear, true, 2, TimeSpan.FromSeconds(1), null, ref state, _randomizer) + .Should() + .Be(TimeSpan.FromSeconds(3.75)); } [Fact] @@ -86,13 +137,38 @@ public void Exponential_Ok() { double state = 0; - RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); - RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); - RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 0, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 1, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 2, TimeSpan.Zero, null, ref state, _randomizer).Should().Be(TimeSpan.Zero); + + RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 0, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1)); + RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 1, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2)); + RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 2, TimeSpan.FromSeconds(1), null, ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(4)); + } + + [InlineData(DelayBackoffType.Linear, false)] + [InlineData(DelayBackoffType.Exponential, false)] + [InlineData(DelayBackoffType.Constant, false)] + [InlineData(DelayBackoffType.Linear, true)] + [InlineData(DelayBackoffType.Exponential, true)] + [InlineData(DelayBackoffType.Constant, true)] + [Theory] + public void MaxDelay_Ok(DelayBackoffType type, bool jitter) + { + _randomizer = () => 0.5; + var expected = jitter ? TimeSpan.FromSeconds(0.875) : TimeSpan.FromSeconds(1); + double state = 0; + + RetryHelper.GetRetryDelay(type, jitter, 2, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(expected); + } + + [Fact] + public void MaxDelay_DelayLessThanMaxDelay_Respected() + { + double state = 0; + var expected = TimeSpan.FromSeconds(1); - RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1)); - RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2)); - RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(4)); + RetryHelper.GetRetryDelay(DelayBackoffType.Constant, false, 2, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), ref state, _randomizer).Should().Be(expected); } [Fact] @@ -100,7 +176,7 @@ public void GetRetryDelay_Overflow_ReturnsMaxTimeSpan() { double state = 0; - RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 1000, TimeSpan.FromDays(1), ref state, _randomizer).Should().Be(TimeSpan.MaxValue); + RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 1000, TimeSpan.FromDays(1), null, ref state, _randomizer).Should().Be(TimeSpan.MaxValue); } [InlineData(1)] @@ -144,7 +220,7 @@ private static IReadOnlyList GetExponentialWithJitterBackoff(bool cont for (int i = 0; i < retryCount; i++) { - result.Add(RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, true, i, baseDelay, ref state, random)); + result.Add(RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, true, i, baseDelay, null, ref state, random)); } return result; diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs index c41a69ca185..d0727bda922 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs @@ -243,6 +243,30 @@ public async void OnRetry_EnsureCorrectArguments() delays[2].Should().Be(TimeSpan.FromSeconds(6)); } + [Fact] + public async void MaxDelay_EnsureRespected() + { + var delays = new List(); + _options.OnRetry = args => + { + delays.Add(args.RetryDelay); + return default; + }; + + _options.ShouldHandle = args => PredicateResult.True(); + _options.MaxRetryAttempts = 3; + _options.BackoffType = DelayBackoffType.Linear; + _options.MaxDelay = TimeSpan.FromMilliseconds(123); + + var sut = CreateSut(); + + await ExecuteAndAdvance(sut); + + delays[0].Should().Be(TimeSpan.FromMilliseconds(123)); + delays[1].Should().Be(TimeSpan.FromMilliseconds(123)); + delays[2].Should().Be(TimeSpan.FromMilliseconds(123)); + } + [Fact] public async Task OnRetry_EnsureExecutionTime() { diff --git a/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs b/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs index c787301799f..f0d3b8f1c7c 100644 --- a/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs @@ -44,7 +44,8 @@ public void InvalidOptions() DelayGenerator = null!, OnRetry = null!, MaxRetryAttempts = -3, - Delay = TimeSpan.MinValue + Delay = TimeSpan.MinValue, + MaxDelay = TimeSpan.FromSeconds(-10) }; options.Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid Options"))) @@ -56,6 +57,7 @@ Invalid Options Validation Errors: The field MaxRetryAttempts must be between 1 and 2147483647. The field Delay must be between 00:00:00 and 1.00:00:00. + The field MaxDelay must be between 00:00:00 and 1.00:00:00. The ShouldHandle field is required. """); }