Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow jitter for all backoff types #1445

Merged
merged 2 commits into from
Jul 28, 2023
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
5 changes: 3 additions & 2 deletions src/Polly.Core/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,8 @@ Polly.Retry.OnRetryArguments.RetryDelay.get -> System.TimeSpan
Polly.Retry.OnRetryArguments.RetryDelay.init -> void
Polly.Retry.RetryBackoffType
Polly.Retry.RetryBackoffType.Constant = 0 -> Polly.Retry.RetryBackoffType
Polly.Retry.RetryBackoffType.Exponential = 2 -> Polly.Retry.RetryBackoffType
Polly.Retry.RetryBackoffType.ExponentialWithJitter = 3 -> Polly.Retry.RetryBackoffType
Polly.Retry.RetryBackoffType.Linear = 1 -> Polly.Retry.RetryBackoffType
Polly.Retry.RetryBackoffType.Exponential = 2 -> Polly.Retry.RetryBackoffType
Polly.Retry.RetryDelayArguments
Polly.Retry.RetryDelayArguments.Attempt.get -> int
Polly.Retry.RetryDelayArguments.Attempt.init -> void
Expand Down Expand Up @@ -350,6 +349,8 @@ Polly.Retry.RetryStrategyOptions<TResult>.RetryDelayGenerator.set -> void
Polly.Retry.RetryStrategyOptions<TResult>.RetryStrategyOptions() -> void
Polly.Retry.RetryStrategyOptions<TResult>.ShouldHandle.get -> System.Func<Polly.OutcomeArguments<TResult, Polly.Retry.RetryPredicateArguments>, System.Threading.Tasks.ValueTask<bool>>!
Polly.Retry.RetryStrategyOptions<TResult>.ShouldHandle.set -> void
Polly.Retry.RetryStrategyOptions<TResult>.UseJitter.get -> bool
Polly.Retry.RetryStrategyOptions<TResult>.UseJitter.set -> void
Polly.RetryResilienceStrategyBuilderExtensions
Polly.Telemetry.ExecutionAttemptArguments
Polly.Telemetry.ExecutionAttemptArguments.Attempt.get -> int
Expand Down
13 changes: 0 additions & 13 deletions src/Polly.Core/Retry/RetryBackoffType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,4 @@ public enum RetryBackoffType
/// 200ms, 400ms, 800ms.
/// </example>
Exponential,

/// <summary>
/// Exponential delay with randomization retry type,
/// making sure to mitigate any correlations.
/// </summary>
/// <example>
/// 850ms, 1455ms, 3060ms.
/// </example>
/// <remarks>
/// In transient failures handling scenarios, this is the
/// <see href=" https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry#new-jitter-recommendation"> recommended retry type</see>.
/// </remarks>
ExponentialWithJitter,
}
19 changes: 17 additions & 2 deletions src/Polly.Core/Retry/RetryHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@ namespace Polly.Retry;

internal static class RetryHelper
{
private const double JitterFactor = 0.5;

private const double ExponentialFactor = 2.0;

public static bool IsValidDelay(TimeSpan delay) => delay >= TimeSpan.Zero;

public static TimeSpan GetRetryDelay(RetryBackoffType type, int attempt, TimeSpan baseDelay, ref double state, Func<double> randomizer)
public static TimeSpan GetRetryDelay(RetryBackoffType type, bool jitter, int attempt, TimeSpan baseDelay, ref double state, Func<double> randomizer)
{
if (baseDelay == TimeSpan.Zero)
{
return baseDelay;
}

if (jitter)
{
return type switch
{
RetryBackoffType.Constant => ApplyJitter(baseDelay, randomizer),
RetryBackoffType.Linear => ApplyJitter(TimeSpan.FromMilliseconds((attempt + 1) * baseDelay.TotalMilliseconds), randomizer),
RetryBackoffType.Exponential => DecorrelatedJitterBackoffV2(attempt, baseDelay, ref state, randomizer),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "The retry backoff type is not supported.")
};
}

return type switch
{
RetryBackoffType.Constant => baseDelay,
Expand All @@ -23,7 +36,6 @@ public static TimeSpan GetRetryDelay(RetryBackoffType type, int attempt, TimeSpa
RetryBackoffType.Linear => (attempt + 1) * baseDelay,
RetryBackoffType.Exponential => Math.Pow(ExponentialFactor, attempt) * baseDelay,
#endif
RetryBackoffType.ExponentialWithJitter => DecorrelatedJitterBackoffV2(attempt, baseDelay, ref state, randomizer),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "The retry backoff type is not supported.")
};
}
Expand Down Expand Up @@ -72,4 +84,7 @@ private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDe

return TimeSpan.FromTicks((long)Math.Min(formulaIntrinsicValue * RpScalingFactor * targetTicksFirstDelay, maxTimeSpanDouble));
}

private static TimeSpan ApplyJitter(TimeSpan delay, Func<double> randomizer)
=> TimeSpan.FromMilliseconds(delay.TotalMilliseconds + ((delay.TotalMilliseconds * JitterFactor) * randomizer()));
}
5 changes: 4 additions & 1 deletion src/Polly.Core/Retry/RetryResilienceStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public RetryResilienceStrategy(
RetryCount = options.RetryCount;
OnRetry = options.OnRetry;
DelayGenerator = options.RetryDelayGenerator;
UseJitter = options.UseJitter;

_timeProvider = timeProvider;
_telemetry = telemetry;
Expand All @@ -39,6 +40,8 @@ public RetryResilienceStrategy(

public Func<OutcomeArguments<T, RetryDelayArguments>, ValueTask<TimeSpan>>? DelayGenerator { get; }

public bool UseJitter { get; }

public Func<OutcomeArguments<T, OnRetryArguments>, ValueTask>? OnRetry { get; }

protected override async ValueTask<Outcome<T>> ExecuteCallbackAsync<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
Expand All @@ -62,7 +65,7 @@ protected override async ValueTask<Outcome<T>> ExecuteCallbackAsync<TState>(Func
return outcome;
}

var delay = RetryHelper.GetRetryDelay(BackoffType, attempt, BaseDelay, ref retryState, _randomizer);
var delay = RetryHelper.GetRetryDelay(BackoffType, UseJitter, attempt, BaseDelay, ref retryState, _randomizer);
if (DelayGenerator is not null)
{
var delayArgs = new OutcomeArguments<T, RetryDelayArguments>(context, outcome, new RetryDelayArguments(attempt, delay));
Expand Down
15 changes: 12 additions & 3 deletions src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ public class RetryStrategyOptions<TResult> : ResilienceStrategyOptions
/// </value>
public RetryBackoffType BackoffType { get; set; } = RetryConstants.DefaultBackoffType;

/// <summary>
/// Gets or sets a value indicating whether jitter should be used when calculating the backoff delay between retries.
/// </summary>
/// <remarks>
/// See <see href="https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry#new-jitter-recommendation"/> for more details
/// on how jitter can improve the resilience when the retries are correlated.
/// </remarks>
/// <value>
/// The default value is <see langword="false"/>.
/// </value>
public bool UseJitter { get; set; }

#pragma warning disable IL2026 // Addressed with DynamicDependency on ValidationHelper.Validate method
/// <summary>
/// Gets or sets the base delay between retries.
Expand All @@ -45,9 +57,6 @@ public class RetryStrategyOptions<TResult> : ResilienceStrategyOptions
/// <see cref="RetryBackoffType.Exponential"/>: Represents the median delay to target before the first retry.
/// </item>
/// <item>
/// <see cref="RetryBackoffType.ExponentialWithJitter"/>: Represents the median delay to target before the first retry.
/// </item>
/// <item>
/// <see cref="RetryBackoffType.Linear"/>: Represents the initial delay, the following delays increasing linearly with this value.
/// </item>
/// <item>
Expand Down
77 changes: 50 additions & 27 deletions test/Polly.Core.Tests/Retry/RetryHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Polly.Core.Tests.Retry;

public class RetryHelperTests
{
private readonly Func<double> _randomizer = new RandomUtil(0).NextDouble;
private Func<double> _randomizer = new RandomUtil(0).NextDouble;

[Fact]
public void IsValidDelay_Ok()
Expand All @@ -18,58 +18,81 @@ public void IsValidDelay_Ok()
RetryHelper.IsValidDelay(TimeSpan.FromMilliseconds(-1)).Should().BeFalse();
}

[Fact]
public void UnsupportedRetryBackoffType_Throws()
[InlineData(true)]
[InlineData(false)]
[Theory]
public void UnsupportedRetryBackoffType_Throws(bool jitter)
{
RetryBackoffType type = (RetryBackoffType)99;

Assert.Throws<ArgumentOutOfRangeException>(() =>
{
double state = 0;
return RetryHelper.GetRetryDelay(type, 0, TimeSpan.FromSeconds(1), ref state, _randomizer);
return RetryHelper.GetRetryDelay(type, jitter, 0, TimeSpan.FromSeconds(1), ref state, _randomizer);
});
}

[Fact]
public void Constant_Ok()
[InlineData(true)]
[InlineData(false)]
[Theory]
public void Constant_Ok(bool jitter)
{
double state = 0;
if (jitter)
{
_randomizer = () => 0.5;
}

RetryHelper.GetRetryDelay(RetryBackoffType.Constant, jitter, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, jitter, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, jitter, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);

RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
var expected = !jitter ? TimeSpan.FromSeconds(1) : TimeSpan.FromSeconds(1.25);

RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, jitter, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(expected);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, jitter, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(expected);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, jitter, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(expected);
}

[Fact]
public void Linear_Ok()
[InlineData(true)]
[InlineData(false)]
[Theory]
public void Linear_Ok(bool jitter)
{
double state = 0;

RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);

if (jitter)
{
_randomizer = () => 0.5;

RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(3));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1.25));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2.5));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(3.75));
}
else
{
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, jitter, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(3));
}
}

[Fact]
public void Exponential_Ok()
{
double state = 0;

RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, false, 0, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, false, 1, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, false, 2, TimeSpan.Zero, ref state, _randomizer).Should().Be(TimeSpan.Zero);

RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(4));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, false, 0, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, false, 1, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(2));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, false, 2, TimeSpan.FromSeconds(1), ref state, _randomizer).Should().Be(TimeSpan.FromSeconds(4));
}

[InlineData(1)]
Expand Down Expand Up @@ -113,7 +136,7 @@ private static IReadOnlyList<TimeSpan> GetExponentialWithJitterBackoff(bool cont

for (int i = 0; i < retryCount; i++)
{
result.Add(RetryHelper.GetRetryDelay(RetryBackoffType.ExponentialWithJitter, i, baseDelay, ref state, random));
result.Add(RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, true, i, baseDelay, ref state, random));
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public void AddRetry_InvalidOptions_Throws()
[Fact]
public void GetAggregatedDelay_ShouldReturnTheSameValue()
{
var options = new RetryStrategyOptions { BackoffType = RetryBackoffType.ExponentialWithJitter };
var options = new RetryStrategyOptions { BackoffType = RetryBackoffType.Exponential, UseJitter = true };

var delay = GetAggregatedDelay(options);
GetAggregatedDelay(options).Should().Be(delay);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public void StrategiesPerEndpoint_1365()
{
builder.AddRetry(new()
{
BackoffType = RetryBackoffType.ExponentialWithJitter,
BackoffType = RetryBackoffType.Exponential,
UseJitter = true,
RetryCount = endpointOptions.Retries,
StrategyName = $"{context.StrategyKey.EndpointName}-Retry",
});
Expand Down