Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<PackageVersion Include="Cake.FileHelpers" Version="7.0.0" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
<PackageVersion Include="Flurl.Http.Signed" Version="4.0.2" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
<PackageVersion Include="FSharp.Core" Version="8.0.200" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="IcedTasks" Version="0.11.4" />
Expand Down
72 changes: 36 additions & 36 deletions src/Polly.Core/Retry/RetryHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,37 +45,16 @@ public static TimeSpan GetRetryDelay(
}
}

private static TimeSpan GetRetryDelayCore(DelayBackoffType type, bool jitter, int attempt, TimeSpan baseDelay, ref double state, Func<double> randomizer)
#pragma warning disable IDE0047 // Remove unnecessary parentheses which offer less mental gymnastics
internal static TimeSpan ApplyJitter(TimeSpan delay, Func<double> 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

/// <summary>
/// Generates sleep durations in an exponentially backing-off, jittered manner, making sure to mitigate any correlations.
Expand All @@ -93,7 +72,7 @@ private static TimeSpan GetRetryDelayCore(DelayBackoffType type, bool jitter, in
/// <remarks>
/// This code was adopted from https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry/blob/master/src/Polly.Contrib.WaitAndRetry/Backoff.DecorrelatedJitterV2.cs.
/// </remarks>
private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDelay, ref double prev, Func<double> randomizer)
internal static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDelay, ref double prev, Func<double> 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
Expand Down Expand Up @@ -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<double> randomizer)
private static TimeSpan GetRetryDelayCore(DelayBackoffType type, bool jitter, int attempt, TimeSpan baseDelay, ref double state, Func<double> 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
}
1 change: 1 addition & 0 deletions test/Polly.Core.Tests/Polly.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<Compile Include="..\Shared\TestCancellation.cs" Link="TestCancellation.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsCheck.Xunit" />
<PackageReference Include="Microsoft.Bcl.TimeProvider" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" />
Expand Down
57 changes: 56 additions & 1 deletion test/Polly.Core.Tests/Retry/RetryHelperTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using FsCheck;
using FsCheck.Fluent;
using Polly.Contrib.WaitAndRetry;
using Polly.Retry;
using Polly.Utils;
Expand Down Expand Up @@ -261,7 +263,38 @@ public void ExponentialWithJitter_EnsureRandomness()
delays1.ShouldAllBe(delay => delay > TimeSpan.Zero);
}

private static IReadOnlyList<TimeSpan> GetExponentialWithJitterBackoff(bool contrib, TimeSpan baseDelay, int retryCount, Func<double>? 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<TimeSpan> GetExponentialWithJitterBackoff(bool contrib, TimeSpan baseDelay, int retryCount, Func<double>? randomizer = null)
{
if (contrib)
{
Expand All @@ -279,4 +312,26 @@ private static IReadOnlyList<TimeSpan> GetExponentialWithJitterBackoff(bool cont

return result;
}

public static class Arbitraries
{
public static Arbitrary<int> PositiveInteger()
{
var minimum = 1;
var maximum = 2056;
var generator = Gen.Choose(minimum, maximum);

return Arb.From(generator);
}

public static Arbitrary<TimeSpan> 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);
}
}
}
Loading