Skip to content

Commit

Permalink
Retries with jitter
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk committed Jul 28, 2023
1 parent 9a63e4e commit 1d7a807
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 50 deletions.
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 the 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

0 comments on commit 1d7a807

Please sign in to comment.