From a98285403961172667d22b5fb05ff1122043da61 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 25 Jun 2024 09:39:03 +0100 Subject: [PATCH 1/7] Suppress xUnit1042 Suppress `xUnit1042` warnings. --- .../Utils/CancellationTokenSourcePoolTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/Polly.Core.Tests/Utils/CancellationTokenSourcePoolTests.cs b/test/Polly.Core.Tests/Utils/CancellationTokenSourcePoolTests.cs index 4774aa9844a..b522ad52c34 100644 --- a/test/Polly.Core.Tests/Utils/CancellationTokenSourcePoolTests.cs +++ b/test/Polly.Core.Tests/Utils/CancellationTokenSourcePoolTests.cs @@ -24,7 +24,9 @@ public void ArgValidation_Ok() pool.Get(System.Threading.Timeout.InfiniteTimeSpan).Should().NotBeNull(); } +#pragma warning disable xUnit1042 // The member referenced by the MemberData attribute returns untyped data rows [MemberData(nameof(TimeProviders))] +#pragma warning restore xUnit1042 // The member referenced by the MemberData attribute returns untyped data rows [Theory] public void RentReturn_Reusable_EnsureProperBehavior(object timeProvider) { @@ -48,7 +50,9 @@ public void RentReturn_Reusable_EnsureProperBehavior(object timeProvider) #endif } +#pragma warning disable xUnit1042 // The member referenced by the MemberData attribute returns untyped data rows [MemberData(nameof(TimeProviders))] +#pragma warning restore xUnit1042 // The member referenced by the MemberData attribute returns untyped data rows [Theory] public void RentReturn_NotReusable_EnsureProperBehavior(object timeProvider) { @@ -63,7 +67,9 @@ public void RentReturn_NotReusable_EnsureProperBehavior(object timeProvider) cts2.Token.Should().NotBeNull(); } +#pragma warning disable xUnit1042 // The member referenced by the MemberData attribute returns untyped data rows [MemberData(nameof(TimeProviders))] +#pragma warning restore xUnit1042 // The member referenced by the MemberData attribute returns untyped data rows [Theory] public async Task Rent_Cancellable_EnsureCancelled(object timeProvider) { @@ -80,7 +86,9 @@ public async Task Rent_Cancellable_EnsureCancelled(object timeProvider) await TestUtilities.AssertWithTimeoutAsync(() => cts.IsCancellationRequested.Should().BeTrue()); } +#pragma warning disable xUnit1042 // The member referenced by the MemberData attribute returns untyped data rows [MemberData(nameof(TimeProviders))] +#pragma warning restore xUnit1042 // The member referenced by the MemberData attribute returns untyped data rows [Theory] public async Task Rent_NotCancellable_EnsureNotCancelled(object timeProvider) { From a2fea03ded96cea9e7e3b8a8e3ba79c677db0044 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 25 Jun 2024 09:39:22 +0100 Subject: [PATCH 2/7] Add test to reproduce issue Add repro for #2163. --- .../Issues/IssuesTests.InfiniteRetry_2163.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs diff --git a/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs b/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs new file mode 100644 index 00000000000..d2781fcfef3 --- /dev/null +++ b/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Time.Testing; +using Polly.Retry; + +namespace Polly.Core.Tests.Issues; + +public partial class IssuesTests +{ + [Fact(Timeout = 15_000)] + public async Task InfiniteRetry_Delay_Does_Not_Overflow_2163() + { + // Arrange + var options = new RetryStrategyOptions + { + BackoffType = DelayBackoffType.Exponential, + Delay = TimeSpan.FromSeconds(2), + MaxDelay = TimeSpan.FromSeconds(30), + MaxRetryAttempts = int.MaxValue, + UseJitter = true, + OnRetry = (args) => + { + args.RetryDelay.Should().BeGreaterThan(TimeSpan.Zero, $"RetryDelay is less than zero after {args.AttemptNumber} attempts"); + return default; + }, + }; + + var listener = new FakeTelemetryListener(); + var telemetry = TestUtilities.CreateResilienceTelemetry(listener); + var timeProvider = new FakeTimeProvider(); + + var strategy = new RetryResilienceStrategy(options, timeProvider, telemetry); + var pipeline = strategy.AsPipeline(); + + int attempts = 0; + int succeedAfter = 2049; + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + // Act + var executing = pipeline.ExecuteAsync((_) => + { + if (attempts++ < succeedAfter) + { + throw new InvalidOperationException("Simulated exception"); + } + + return new ValueTask(true); + }, cts.Token); + + while (!executing.IsCompleted && !cts.IsCancellationRequested) + { + timeProvider.Advance(TimeSpan.FromSeconds(1)); + } + + // Assert + cts.Token.ThrowIfCancellationRequested(); + + var actual = await executing; + + actual.Should().BeTrue(); + attempts.Should().Be(succeedAfter); + } +} From ebf2277279f78d7ee7fe5f42806ceddd52c18b24 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 25 Jun 2024 09:43:11 +0100 Subject: [PATCH 3/7] Add assert for delay Add an assertion for the delay's value not being negative. --- src/Polly.Core/Retry/RetryResilienceStrategy.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Polly.Core/Retry/RetryResilienceStrategy.cs b/src/Polly.Core/Retry/RetryResilienceStrategy.cs index 0b9dfbacde1..484b78d48cf 100644 --- a/src/Polly.Core/Retry/RetryResilienceStrategy.cs +++ b/src/Polly.Core/Retry/RetryResilienceStrategy.cs @@ -75,6 +75,8 @@ protected internal override async ValueTask> ExecuteCore(Func } } + Debug.Assert(delay >= TimeSpan.Zero, "The delay cannot be negative."); + var onRetryArgs = new OnRetryArguments(context, outcome, attempt, delay, executionTime); _telemetry.Report, T>(new(ResilienceEventSeverity.Warning, RetryConstants.OnRetryEvent), onRetryArgs); From f8584441f90a7bb00a9cfac003751e8d76c4c8f7 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 25 Jun 2024 10:20:08 +0100 Subject: [PATCH 4/7] Fix retry delay going negative - Fix retry delays going negative with `DelayBackoffType.Exponential` after 1,024 retries. - Only compute the effective maximum `TimeSpan` once. - Improve regression test. - Assert delays are positive. Resolves #2163. --- src/Polly.Core/Retry/RetryHelper.cs | 24 ++++++++++++++----- .../Issues/IssuesTests.InfiniteRetry_2163.cs | 13 +++++----- .../Retry/RetryHelperTests.cs | 6 ++++- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/Polly.Core/Retry/RetryHelper.cs b/src/Polly.Core/Retry/RetryHelper.cs index 35d3e5825eb..301a19b28ce 100644 --- a/src/Polly.Core/Retry/RetryHelper.cs +++ b/src/Polly.Core/Retry/RetryHelper.cs @@ -6,6 +6,11 @@ internal static class RetryHelper private const double ExponentialFactor = 2.0; + // Upper-bound to prevent overflow beyond TimeSpan.MaxValue. Potential truncation during conversion from double to long + // (as described at https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions) + // is avoided by the arbitrary subtraction of 1000. Validated by unit-test Backoff_should_not_overflow_to_give_negative_timespan. + private static readonly double MaxTimeSpanDouble = (double)TimeSpan.MaxValue.Ticks - 1000; + public static bool IsValidDelay(TimeSpan delay) => delay >= TimeSpan.Zero; public static TimeSpan GetRetryDelay( @@ -101,20 +106,27 @@ private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDe // This factor allows the median values to fall approximately at 1, 2, 4 etc seconds, instead of 1.4, 2.8, 5.6, 11.2. const double RpScalingFactor = 1 / 1.4d; - // Upper-bound to prevent overflow beyond TimeSpan.MaxValue. Potential truncation during conversion from double to long - // (as described at https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions) - // is avoided by the arbitrary subtraction of 1000. Validated by unit-test Backoff_should_not_overflow_to_give_negative_timespan. - double maxTimeSpanDouble = (double)TimeSpan.MaxValue.Ticks - 1000; - long targetTicksFirstDelay = baseDelay.Ticks; double t = attempt + randomizer(); double next = Math.Pow(ExponentialFactor, t) * Math.Tanh(Math.Sqrt(PFactor * t)); + // At t >=1024, the above will tend to infinity which would otherwise cause the + // ticks to go negative. See https://github.com/App-vNext/Polly/issues/2163. + if (double.IsInfinity(next)) + { + prev = next; + return TimeSpan.FromTicks((long)MaxTimeSpanDouble); + } + double formulaIntrinsicValue = next - prev; prev = next; - return TimeSpan.FromTicks((long)Math.Min(formulaIntrinsicValue * RpScalingFactor * targetTicksFirstDelay, maxTimeSpanDouble)); + long ticks = (long)Math.Min(formulaIntrinsicValue * RpScalingFactor * targetTicksFirstDelay, MaxTimeSpanDouble); + + Debug.Assert(ticks >= 0, "ticks cannot be negative"); + + return TimeSpan.FromTicks(ticks); } #pragma warning disable IDE0047 // Remove unnecessary parentheses which offer less mental gymnastics diff --git a/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs b/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs index d2781fcfef3..559c6b88574 100644 --- a/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs +++ b/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs @@ -9,6 +9,9 @@ public partial class IssuesTests public async Task InfiniteRetry_Delay_Does_Not_Overflow_2163() { // Arrange + int attempts = 0; + int succeedAfter = 2049; + var options = new RetryStrategyOptions { BackoffType = DelayBackoffType.Exponential, @@ -19,6 +22,7 @@ public async Task InfiniteRetry_Delay_Does_Not_Overflow_2163() OnRetry = (args) => { args.RetryDelay.Should().BeGreaterThan(TimeSpan.Zero, $"RetryDelay is less than zero after {args.AttemptNumber} attempts"); + attempts++; return default; }, }; @@ -30,17 +34,14 @@ public async Task InfiniteRetry_Delay_Does_Not_Overflow_2163() var strategy = new RetryResilienceStrategy(options, timeProvider, telemetry); var pipeline = strategy.AsPipeline(); - int attempts = 0; - int succeedAfter = 2049; - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var cts = new CancellationTokenSource(Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(10)); // Act var executing = pipeline.ExecuteAsync((_) => { - if (attempts++ < succeedAfter) + if (attempts < succeedAfter) { - throw new InvalidOperationException("Simulated exception"); + throw new InvalidOperationException("Not enough attempts yet."); } return new ValueTask(true); diff --git a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs index b5988eac425..23350719734 100644 --- a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs @@ -187,6 +187,7 @@ public void GetRetryDelay_OverflowWithMaxDelay_ReturnsMaxDelay() RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 1000, TimeSpan.FromDays(1), TimeSpan.FromDays(2), ref state, _randomizer).Should().Be(TimeSpan.FromDays(2)); } + [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] @@ -194,7 +195,8 @@ public void GetRetryDelay_OverflowWithMaxDelay_ReturnsMaxDelay() [InlineData(10)] [InlineData(100)] [InlineData(1000)] - [Theory] + [InlineData(1024)] + [InlineData(1025)] public void ExponentialWithJitter_Ok(int count) { var delay = TimeSpan.FromSeconds(7.8); @@ -203,6 +205,7 @@ public void ExponentialWithJitter_Ok(int count) newDelays.Should().ContainInConsecutiveOrder(oldDelays); newDelays.Should().HaveCount(oldDelays.Count); + newDelays.Should().AllSatisfy(delay => delay.Should().BePositive()); } [Fact] @@ -213,6 +216,7 @@ public void ExponentialWithJitter_EnsureRandomness() var delays2 = GetExponentialWithJitterBackoff(false, delay, 100, RandomUtil.Instance.NextDouble); delays1.SequenceEqual(delays2).Should().BeFalse(); + delays1.Should().AllSatisfy(delay => delay.Should().BePositive()); } private static IReadOnlyList GetExponentialWithJitterBackoff(bool contrib, TimeSpan baseDelay, int retryCount, Func? randomizer = null) From d31e84af3ff498e829f54d5b7a24dedcee2ee6b8 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 25 Jun 2024 10:37:44 +0100 Subject: [PATCH 5/7] Add more unit tests - Add more unit tests related to the exponential delay becoming negative. - Remove redundant Stryker comment. --- src/Polly.Core/Retry/RetryHelper.cs | 1 - .../Retry/RetryHelperTests.cs | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Polly.Core/Retry/RetryHelper.cs b/src/Polly.Core/Retry/RetryHelper.cs index 301a19b28ce..2d91f5667c9 100644 --- a/src/Polly.Core/Retry/RetryHelper.cs +++ b/src/Polly.Core/Retry/RetryHelper.cs @@ -26,7 +26,6 @@ public static TimeSpan GetRetryDelay( { 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) { return maxDelay.Value; diff --git a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs index 23350719734..25d4b38c18d 100644 --- a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs @@ -187,6 +187,65 @@ public void GetRetryDelay_OverflowWithMaxDelay_ReturnsMaxDelay() RetryHelper.GetRetryDelay(DelayBackoffType.Exponential, false, 1000, TimeSpan.FromDays(1), TimeSpan.FromDays(2), ref state, _randomizer).Should().Be(TimeSpan.FromDays(2)); } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(1024)] + [InlineData(1025)] + public void GetRetryDelay_Exponential_Is_Positive_When_No_Maximum_Delay(int attempt) + { + var jitter = true; + var type = DelayBackoffType.Exponential; + + var baseDelay = TimeSpan.FromSeconds(2); + TimeSpan? maxDelay = null; + + var random = new RandomUtil(0).NextDouble; + double state = 0; + + var first = RetryHelper.GetRetryDelay(type, jitter, attempt, baseDelay, maxDelay, ref state, random); + var second = RetryHelper.GetRetryDelay(type, jitter, attempt, baseDelay, maxDelay, ref state, random); + + first.Should().BePositive(); + second.Should().BePositive(); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(1024)] + [InlineData(1025)] + public void GetRetryDelay_Exponential_Does_Not_Exceed_MaxDelay(int attempt) + { + var jitter = true; + var type = DelayBackoffType.Exponential; + + var baseDelay = TimeSpan.FromSeconds(2); + var maxDelay = TimeSpan.FromSeconds(30); + + var random = new RandomUtil(0).NextDouble; + double state = 0; + + var first = RetryHelper.GetRetryDelay(type, jitter, attempt, baseDelay, maxDelay, ref state, random); + var second = RetryHelper.GetRetryDelay(type, jitter, attempt, baseDelay, maxDelay, ref state, random); + + first.Should().BePositive(); + first.Should().BeLessThanOrEqualTo(maxDelay); + + second.Should().BePositive(); + second.Should().BeLessThanOrEqualTo(maxDelay); + } + [Theory] [InlineData(1)] [InlineData(2)] From 6340601255aabe046d97793e5a71c150c25b9fdb Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Tue, 25 Jun 2024 11:05:46 +0100 Subject: [PATCH 6/7] Restore Styker comment Turns out it's the second part that's (now) unreachable. --- src/Polly.Core/Retry/RetryHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Polly.Core/Retry/RetryHelper.cs b/src/Polly.Core/Retry/RetryHelper.cs index 2d91f5667c9..301a19b28ce 100644 --- a/src/Polly.Core/Retry/RetryHelper.cs +++ b/src/Polly.Core/Retry/RetryHelper.cs @@ -26,6 +26,7 @@ public static TimeSpan GetRetryDelay( { 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) { return maxDelay.Value; From 1300d7f137c25e7da3fe76f7906dbdb83b01bc60 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 25 Jun 2024 12:43:15 +0100 Subject: [PATCH 7/7] Address feedback - Rename field. - Remove redundant comment. - Simplify repro slightly. - Use `TheoryData` for retry test cases. --- src/Polly.Core/Retry/RetryHelper.cs | 8 ++-- .../Issues/IssuesTests.InfiniteRetry_2163.cs | 12 ++--- .../Retry/RetryHelperTests.cs | 48 ++++++++----------- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/src/Polly.Core/Retry/RetryHelper.cs b/src/Polly.Core/Retry/RetryHelper.cs index 301a19b28ce..1acbbf8f6cc 100644 --- a/src/Polly.Core/Retry/RetryHelper.cs +++ b/src/Polly.Core/Retry/RetryHelper.cs @@ -8,8 +8,8 @@ internal static class RetryHelper // Upper-bound to prevent overflow beyond TimeSpan.MaxValue. Potential truncation during conversion from double to long // (as described at https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions) - // is avoided by the arbitrary subtraction of 1000. Validated by unit-test Backoff_should_not_overflow_to_give_negative_timespan. - private static readonly double MaxTimeSpanDouble = (double)TimeSpan.MaxValue.Ticks - 1000; + // is avoided by the arbitrary subtraction of 1,000. + private static readonly double MaxTimeSpanTicks = (double)TimeSpan.MaxValue.Ticks - 1_000; public static bool IsValidDelay(TimeSpan delay) => delay >= TimeSpan.Zero; @@ -116,13 +116,13 @@ private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDe if (double.IsInfinity(next)) { prev = next; - return TimeSpan.FromTicks((long)MaxTimeSpanDouble); + return TimeSpan.FromTicks((long)MaxTimeSpanTicks); } double formulaIntrinsicValue = next - prev; prev = next; - long ticks = (long)Math.Min(formulaIntrinsicValue * RpScalingFactor * targetTicksFirstDelay, MaxTimeSpanDouble); + long ticks = (long)Math.Min(formulaIntrinsicValue * RpScalingFactor * targetTicksFirstDelay, MaxTimeSpanTicks); Debug.Assert(ticks >= 0, "ticks cannot be negative"); diff --git a/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs b/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs index 559c6b88574..518e47549fc 100644 --- a/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs +++ b/test/Polly.Core.Tests/Issues/IssuesTests.InfiniteRetry_2163.cs @@ -12,7 +12,7 @@ public async Task InfiniteRetry_Delay_Does_Not_Overflow_2163() int attempts = 0; int succeedAfter = 2049; - var options = new RetryStrategyOptions + var options = new RetryStrategyOptions { BackoffType = DelayBackoffType.Exponential, Delay = TimeSpan.FromSeconds(2), @@ -25,13 +25,14 @@ public async Task InfiniteRetry_Delay_Does_Not_Overflow_2163() attempts++; return default; }, + ShouldHandle = (args) => new ValueTask(!args.Outcome.Result), }; var listener = new FakeTelemetryListener(); var telemetry = TestUtilities.CreateResilienceTelemetry(listener); var timeProvider = new FakeTimeProvider(); - var strategy = new RetryResilienceStrategy(options, timeProvider, telemetry); + var strategy = new RetryResilienceStrategy(options, timeProvider, telemetry); var pipeline = strategy.AsPipeline(); using var cts = new CancellationTokenSource(Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(10)); @@ -39,12 +40,7 @@ public async Task InfiniteRetry_Delay_Does_Not_Overflow_2163() // Act var executing = pipeline.ExecuteAsync((_) => { - if (attempts < succeedAfter) - { - throw new InvalidOperationException("Not enough attempts yet."); - } - - return new ValueTask(true); + return new ValueTask(attempts >= succeedAfter); }, cts.Token); while (!executing.IsCompleted && !cts.IsCancellationRequested) diff --git a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs index 25d4b38c18d..be770eca1e9 100644 --- a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs @@ -8,6 +8,24 @@ public class RetryHelperTests { private Func _randomizer = new RandomUtil(0).NextDouble; + public static TheoryData Attempts() + { +#pragma warning disable IDE0028 + return new() + { + 1, + 2, + 3, + 4, + 10, + 100, + 1_000, + 1_024, + 1_025, + }; +#pragma warning restore IDE0028 + } + [Fact] public void IsValidDelay_Ok() { @@ -188,15 +206,7 @@ public void GetRetryDelay_OverflowWithMaxDelay_ReturnsMaxDelay() } [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(10)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(1024)] - [InlineData(1025)] + [MemberData(nameof(Attempts))] public void GetRetryDelay_Exponential_Is_Positive_When_No_Maximum_Delay(int attempt) { var jitter = true; @@ -216,15 +226,7 @@ public void GetRetryDelay_Exponential_Is_Positive_When_No_Maximum_Delay(int atte } [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(10)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(1024)] - [InlineData(1025)] + [MemberData(nameof(Attempts))] public void GetRetryDelay_Exponential_Does_Not_Exceed_MaxDelay(int attempt) { var jitter = true; @@ -247,15 +249,7 @@ public void GetRetryDelay_Exponential_Does_Not_Exceed_MaxDelay(int attempt) } [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(10)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(1024)] - [InlineData(1025)] + [MemberData(nameof(Attempts))] public void ExponentialWithJitter_Ok(int count) { var delay = TimeSpan.FromSeconds(7.8);