diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8ffe9f01af3..a6cd86686b2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,6 +4,7 @@
+
diff --git a/src/Polly.Core/Retry/RetryHelper.cs b/src/Polly.Core/Retry/RetryHelper.cs
index 9736a83375c..d5b66a51210 100644
--- a/src/Polly.Core/Retry/RetryHelper.cs
+++ b/src/Polly.Core/Retry/RetryHelper.cs
@@ -45,37 +45,16 @@ public static TimeSpan GetRetryDelay(
}
}
- private static TimeSpan GetRetryDelayCore(DelayBackoffType type, bool jitter, int attempt, TimeSpan baseDelay, ref double state, Func randomizer)
+#pragma warning disable IDE0047 // Remove unnecessary parentheses which offer less mental gymnastics
+ internal static TimeSpan ApplyJitter(TimeSpan delay, Func randomizer)
{
- if (baseDelay == TimeSpan.Zero)
- {
- return baseDelay;
- }
-
- if (jitter)
- {
- return type switch
- {
- DelayBackoffType.Constant => ApplyJitter(baseDelay, randomizer),
- DelayBackoffType.Linear => ApplyJitter(TimeSpan.FromMilliseconds((attempt + 1) * baseDelay.TotalMilliseconds), randomizer),
- DelayBackoffType.Exponential => DecorrelatedJitterBackoffV2(attempt, baseDelay, ref state, randomizer),
- _ => throw new ArgumentOutOfRangeException(nameof(type), type, "The retry backoff type is not supported.")
- };
- }
+ var offset = (delay.TotalMilliseconds * JitterFactor) / 2;
+ var randomDelay = (delay.TotalMilliseconds * JitterFactor * randomizer()) - offset;
+ var newDelay = delay.TotalMilliseconds + randomDelay;
- return type switch
- {
- DelayBackoffType.Constant => baseDelay,
-#if !NETCOREAPP
- DelayBackoffType.Linear => TimeSpan.FromMilliseconds((attempt + 1) * baseDelay.TotalMilliseconds),
- DelayBackoffType.Exponential => TimeSpan.FromMilliseconds(Math.Pow(ExponentialFactor, attempt) * baseDelay.TotalMilliseconds),
-#else
- DelayBackoffType.Linear => (attempt + 1) * baseDelay,
- DelayBackoffType.Exponential => Math.Pow(ExponentialFactor, attempt) * baseDelay,
-#endif
- _ => throw new ArgumentOutOfRangeException(nameof(type), type, "The retry backoff type is not supported.")
- };
+ return TimeSpan.FromMilliseconds(newDelay);
}
+#pragma warning disable IDE0047 // Remove unnecessary parentheses which offer less mental gymnastics
///
/// Generates sleep durations in an exponentially backing-off, jittered manner, making sure to mitigate any correlations.
@@ -93,7 +72,7 @@ private static TimeSpan GetRetryDelayCore(DelayBackoffType type, bool jitter, in
///
/// This code was adopted from https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry/blob/master/src/Polly.Contrib.WaitAndRetry/Backoff.DecorrelatedJitterV2.cs.
///
- private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDelay, ref double prev, Func randomizer)
+ internal static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDelay, ref double prev, Func randomizer)
{
// The original author/credit for this jitter formula is @george-polevoy .
// Jitter formula used with permission as described at https://github.com/App-vNext/Polly/issues/530#issuecomment-526555979
@@ -131,14 +110,35 @@ private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDe
return TimeSpan.FromTicks(ticks);
}
-#pragma warning disable IDE0047 // Remove unnecessary parentheses which offer less mental gymnastics
- private static TimeSpan ApplyJitter(TimeSpan delay, Func randomizer)
+ private static TimeSpan GetRetryDelayCore(DelayBackoffType type, bool jitter, int attempt, TimeSpan baseDelay, ref double state, Func randomizer)
{
- var offset = (delay.TotalMilliseconds * JitterFactor) / 2;
- var randomDelay = (delay.TotalMilliseconds * JitterFactor * randomizer()) - offset;
- var newDelay = delay.TotalMilliseconds + randomDelay;
+ if (baseDelay == TimeSpan.Zero)
+ {
+ return baseDelay;
+ }
- return TimeSpan.FromMilliseconds(newDelay);
+ if (jitter)
+ {
+ return type switch
+ {
+ DelayBackoffType.Constant => ApplyJitter(baseDelay, randomizer),
+ DelayBackoffType.Linear => ApplyJitter(TimeSpan.FromMilliseconds((attempt + 1) * baseDelay.TotalMilliseconds), randomizer),
+ DelayBackoffType.Exponential => DecorrelatedJitterBackoffV2(attempt, baseDelay, ref state, randomizer),
+ _ => throw new ArgumentOutOfRangeException(nameof(type), type, "The retry backoff type is not supported.")
+ };
+ }
+
+ return type switch
+ {
+ DelayBackoffType.Constant => baseDelay,
+#if !NETCOREAPP
+ DelayBackoffType.Linear => TimeSpan.FromMilliseconds((attempt + 1) * baseDelay.TotalMilliseconds),
+ DelayBackoffType.Exponential => TimeSpan.FromMilliseconds(Math.Pow(ExponentialFactor, attempt) * baseDelay.TotalMilliseconds),
+#else
+ DelayBackoffType.Linear => (attempt + 1) * baseDelay,
+ DelayBackoffType.Exponential => Math.Pow(ExponentialFactor, attempt) * baseDelay,
+#endif
+ _ => throw new ArgumentOutOfRangeException(nameof(type), type, "The retry backoff type is not supported.")
+ };
}
-#pragma warning restore IDE0047 // Remove unnecessary parentheses which offer less mental gymnastics
}
diff --git a/test/Polly.Core.Tests/Polly.Core.Tests.csproj b/test/Polly.Core.Tests/Polly.Core.Tests.csproj
index fed35451ec5..d8c1c4bd18c 100644
--- a/test/Polly.Core.Tests/Polly.Core.Tests.csproj
+++ b/test/Polly.Core.Tests/Polly.Core.Tests.csproj
@@ -14,6 +14,7 @@
+
diff --git a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs
index a2cd64f414d..c2529eeb8a7 100644
--- a/test/Polly.Core.Tests/Retry/RetryHelperTests.cs
+++ b/test/Polly.Core.Tests/Retry/RetryHelperTests.cs
@@ -1,3 +1,5 @@
+using FsCheck;
+using FsCheck.Fluent;
using Polly.Contrib.WaitAndRetry;
using Polly.Retry;
using Polly.Utils;
@@ -261,7 +263,38 @@ public void ExponentialWithJitter_EnsureRandomness()
delays1.ShouldAllBe(delay => delay > TimeSpan.Zero);
}
- private static IReadOnlyList GetExponentialWithJitterBackoff(bool contrib, TimeSpan baseDelay, int retryCount, Func? randomizer = null)
+#if !NETFRAMEWORK
+ [FsCheck.Xunit.Property(Arbitrary = [typeof(Arbitraries)], MaxTest = 10_000)]
+ public void ApplyJitter_Meets_Specification(TimeSpan value)
+ {
+ var delta = value / 2;
+ var floor = value - delta;
+ var ceiling = value + delta;
+
+ var actual = RetryHelper.ApplyJitter(value, RandomUtil.NextDouble);
+
+ actual.ShouldBeGreaterThan(floor);
+ actual.ShouldBeLessThanOrEqualTo(ceiling);
+ }
+
+ [FsCheck.Xunit.Property(Arbitrary = [typeof(Arbitraries)], MaxTest = 10_000)]
+ public void DecorrelatedJitterBackoffV2_Meets_Specification(TimeSpan value, int attempt)
+ {
+ var rawCeiling = value.Ticks * Math.Pow(2, attempt) * 4;
+ var clamped = (long)Math.Clamp(rawCeiling, value.Ticks, TimeSpan.MaxValue.Ticks);
+
+ var floor = value;
+ var ceiling = TimeSpan.FromTicks(clamped - 1);
+
+ var _ = default(double);
+ var actual = RetryHelper.DecorrelatedJitterBackoffV2(attempt, value, ref _, RandomUtil.NextDouble);
+
+ actual.ShouldBeGreaterThan(floor);
+ actual.ShouldBeLessThanOrEqualTo(ceiling);
+ }
+#endif
+
+ private static List GetExponentialWithJitterBackoff(bool contrib, TimeSpan baseDelay, int retryCount, Func? randomizer = null)
{
if (contrib)
{
@@ -279,4 +312,26 @@ private static IReadOnlyList GetExponentialWithJitterBackoff(bool cont
return result;
}
+
+ public static class Arbitraries
+ {
+ public static Arbitrary PositiveInteger()
+ {
+ var minimum = 1;
+ var maximum = 2056;
+ var generator = Gen.Choose(minimum, maximum);
+
+ return Arb.From(generator);
+ }
+
+ public static Arbitrary PositiveTimeSpan()
+ {
+ var minimum = (int)TimeSpan.FromMilliseconds(1).Ticks;
+ var maximum = (int)TimeSpan.FromMinutes(60).Ticks;
+ var generator = Gen.Choose(minimum, maximum)
+ .Select((p) => TimeSpan.FromTicks(p));
+
+ return Arb.From(generator);
+ }
+ }
}