Skip to content

Commit

Permalink
Expose Randomizer property and use it in retry strategy (#1346)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Jun 23, 2023
1 parent 6b38850 commit b60eea8
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 52 deletions.
15 changes: 14 additions & 1 deletion src/Polly.Core/ResilienceStrategyBuilderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ private protected ResilienceStrategyBuilderBase(ResilienceStrategyBuilderBase ot
Properties = other.Properties;
TimeProvider = other.TimeProvider;
OnCreatingStrategy = other.OnCreatingStrategy;
Randomizer = other.Randomizer;
DiagnosticSource = other.DiagnosticSource;
}

/// <summary>
Expand Down Expand Up @@ -70,6 +72,16 @@ private protected ResilienceStrategyBuilderBase(ResilienceStrategyBuilderBase ot
[EditorBrowsable(EditorBrowsableState.Never)]
public DiagnosticSource? DiagnosticSource { get; set; }

/// <summary>
/// Gets or sets the randomizer that is used by strategies that need to generate random numbers.
/// </summary>
/// <remarks>
/// The default randomizer is thread safe and returns values between 0.0 and 1.0.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
[Required]
public Func<double> Randomizer { get; set; } = RandomUtil.Instance.NextDouble;

internal abstract bool IsGenericBuilder { get; }

internal void AddStrategyCore(Func<ResilienceStrategyBuilderContext, ResilienceStrategy> factory, ResilienceStrategyOptions options)
Expand Down Expand Up @@ -118,7 +130,8 @@ private ResilienceStrategy CreateResilienceStrategy(Entry entry)
strategyType: entry.Properties.StrategyType,
timeProvider: TimeProvider,
isGenericBuilder: IsGenericBuilder,
diagnosticSource: DiagnosticSource);
diagnosticSource: DiagnosticSource,
randomizer: Randomizer);

return entry.Factory(context);
}
Expand Down
8 changes: 7 additions & 1 deletion src/Polly.Core/ResilienceStrategyBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Polly;

#pragma warning disable S107

/// <summary>
/// The context used for building an individual resilience strategy.
/// </summary>
Expand All @@ -14,7 +16,8 @@ internal ResilienceStrategyBuilderContext(
string strategyType,
TimeProvider timeProvider,
bool isGenericBuilder,
DiagnosticSource? diagnosticSource)
DiagnosticSource? diagnosticSource,
Func<double> randomizer)
{
BuilderName = builderName;
BuilderProperties = builderProperties;
Expand All @@ -23,6 +26,7 @@ internal ResilienceStrategyBuilderContext(
TimeProvider = timeProvider;
IsGenericBuilder = isGenericBuilder;
Telemetry = TelemetryUtil.CreateTelemetry(diagnosticSource, builderName, builderProperties, strategyName, strategyType);
Randomizer = randomizer;
}

/// <summary>
Expand Down Expand Up @@ -55,5 +59,7 @@ internal ResilienceStrategyBuilderContext(
/// </summary>
internal TimeProvider TimeProvider { get; }

internal Func<double> Randomizer { get; }

internal bool IsGenericBuilder { get; }
}
10 changes: 5 additions & 5 deletions src/Polly.Core/Retry/RetryHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ internal static class RetryHelper

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

public static TimeSpan GetRetryDelay(RetryBackoffType type, int attempt, TimeSpan baseDelay, ref double state, RandomUtil random)
public static TimeSpan GetRetryDelay(RetryBackoffType type, int attempt, TimeSpan baseDelay, ref double state, Func<double> randomizer)
{
if (baseDelay == TimeSpan.Zero)
{
Expand All @@ -23,7 +23,7 @@ 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, random),
RetryBackoffType.ExponentialWithJitter => DecorrelatedJitterBackoffV2(attempt, baseDelay, ref state, randomizer),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "The retry backoff type is not supported.")
};
}
Expand All @@ -40,11 +40,11 @@ public static TimeSpan GetRetryDelay(RetryBackoffType type, int attempt, TimeSpa
/// The actual amount of delay-before-retry for try t may be distributed between 0 and <c>f * (2^(t+1) - 2^(t-1)) for t >= 2;</c>
/// or between 0 and <c>f * 2^(t+1)</c>, for t is 0 or 1.</param>
/// <param name="prev">The previous state value used for calculations.</param>
/// <param name="random">The random utility to use.</param>
/// <param name="randomizer">The generator to use.</param>
/// <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, RandomUtil random)
private 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 All @@ -64,7 +64,7 @@ private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDe

long targetTicksFirstDelay = baseDelay.Ticks;

double t = attempt + random.NextDouble();
double t = attempt + randomizer();
double next = Math.Pow(2, t) * Math.Tanh(Math.Sqrt(PFactor * t));

double formulaIntrinsicValue = next - prev;
Expand Down
9 changes: 5 additions & 4 deletions src/Polly.Core/Retry/RetryResilienceStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Polly.Telemetry;

namespace Polly.Retry;
Expand All @@ -6,14 +7,14 @@ internal sealed class RetryResilienceStrategy<T> : OutcomeResilienceStrategy<T>
{
private readonly TimeProvider _timeProvider;
private readonly ResilienceStrategyTelemetry _telemetry;
private readonly RandomUtil _randomUtil;
private readonly Func<double> _randomizer;

public RetryResilienceStrategy(
RetryStrategyOptions<T> options,
bool isGeneric,
TimeProvider timeProvider,
ResilienceStrategyTelemetry telemetry,
RandomUtil randomUtil)
Func<double> randomizer)
: base(isGeneric)
{
ShouldHandle = options.ShouldHandle;
Expand All @@ -25,7 +26,7 @@ public RetryResilienceStrategy(

_timeProvider = timeProvider;
_telemetry = telemetry;
_randomUtil = randomUtil;
_randomizer = randomizer;
}

public TimeSpan BaseDelay { get; }
Expand Down Expand Up @@ -61,7 +62,7 @@ protected override async ValueTask<Outcome<T>> ExecuteCallbackAsync<TState>(Func
return outcome;
}

var delay = RetryHelper.GetRetryDelay(BackoffType, attempt, BaseDelay, ref retryState, _randomUtil);
var delay = RetryHelper.GetRetryDelay(BackoffType, attempt, BaseDelay, ref retryState, _randomizer);
if (DelayGenerator is not null)
{
var delayArgs = new OutcomeArguments<T, RetryDelayArguments>(context, outcome, new RetryDelayArguments(attempt, delay));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private static TBuilder AddRetryCore<TBuilder, TResult>(this TBuilder builder, R
context.IsGenericBuilder,
context.TimeProvider,
context.Telemetry,
RandomUtil.Instance),
context.Randomizer),
options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public void Ctor_EnsureDefaults()
{
var properties = new ResilienceProperties();
var timeProvider = new FakeTimeProvider();
var context = new ResilienceStrategyBuilderContext("builder-name", properties, "strategy-name", "strategy-type", timeProvider.Object, true, Mock.Of<DiagnosticSource>());
var context = new ResilienceStrategyBuilderContext("builder-name", properties, "strategy-name", "strategy-type", timeProvider.Object, true, Mock.Of<DiagnosticSource>(), () => 1.0);

context.IsGenericBuilder.Should().BeTrue();
context.BuilderName.Should().Be("builder-name");
Expand All @@ -18,6 +18,7 @@ public void Ctor_EnsureDefaults()
context.StrategyType.Should().Be("strategy-type");
context.TimeProvider.Should().Be(timeProvider.Object);
context.Telemetry.Should().NotBeNull();
context.Randomizer.Should().NotBeNull();

context.Telemetry.TelemetrySource.BuilderName.Should().Be("builder-name");
context.Telemetry.TelemetrySource.BuilderProperties.Should().BeSameAs(properties);
Expand Down
36 changes: 28 additions & 8 deletions test/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Moq;
using Polly.Utils;

namespace Polly.Core.Tests;
Expand All @@ -14,6 +15,30 @@ public void Ctor_EnsureDefaults()
builder.Properties.Should().NotBeNull();
builder.TimeProvider.Should().Be(TimeProvider.System);
builder.IsGenericBuilder.Should().BeFalse();
builder.Randomizer.Should().NotBeNull();
}

[Fact]
public void CopyCtor_Ok()
{
var builder = new ResilienceStrategyBuilder
{
TimeProvider = Mock.Of<TimeProvider>(),
BuilderName = "dummy",
Randomizer = () => 0.0,
DiagnosticSource = Mock.Of<DiagnosticSource>(),
OnCreatingStrategy = _ => { },
};

builder.Properties.Set(new ResiliencePropertyKey<string>("dummy"), "dummy");

var other = new ResilienceStrategyBuilder<double>(builder);
other.BuilderName.Should().Be(builder.BuilderName);
other.TimeProvider.Should().Be(builder.TimeProvider);
other.Randomizer.Should().BeSameAs(builder.Randomizer);
other.DiagnosticSource.Should().BeSameAs(builder.DiagnosticSource);
other.OnCreatingStrategy.Should().BeSameAs(builder.OnCreatingStrategy);
other.Properties.GetValue(new ResiliencePropertyKey<string>("dummy"), "").Should().Be("dummy");
}

[Fact]
Expand Down Expand Up @@ -133,10 +158,7 @@ public void AddStrategy_MultipleNonDelegating_Ok()
}

[Fact]
public void Build_Empty_ReturnsNullResilienceStrategy()
{
new ResilienceStrategyBuilder().Build().Should().BeSameAs(NullResilienceStrategy.Instance);
}
public void Build_Empty_ReturnsNullResilienceStrategy() => new ResilienceStrategyBuilder().Build().Should().BeSameAs(NullResilienceStrategy.Instance);

[Fact]
public void AddStrategy_AfterUsed_Throws()
Expand Down Expand Up @@ -259,6 +281,7 @@ public void BuildStrategy_EnsureCorrectContext()
context.BuilderProperties.Should().BeSameAs(builder.Properties);
context.Telemetry.Should().NotBeNull();
context.TimeProvider.Should().Be(builder.TimeProvider);
context.Randomizer.Should().BeSameAs(builder.Randomizer);
verified1 = true;
return new TestResilienceStrategy();
Expand Down Expand Up @@ -312,10 +335,7 @@ public void Build_OnCreatingStrategy_EnsureRespected()
}

[Fact]
public void EmptyOptions_Ok()
{
ResilienceStrategyBuilderExtensions.EmptyOptions.Instance.StrategyType.Should().Be("Empty");
}
public void EmptyOptions_Ok() => ResilienceStrategyBuilderExtensions.EmptyOptions.Instance.StrategyType.Should().Be("Empty");

[Fact]
public void ExecuteAsync_EnsureReceivedCallbackExecutesNextStrategy()
Expand Down
48 changes: 24 additions & 24 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 RandomUtil _randomUtil = new(0);
private readonly Func<double> _randomizer = new RandomUtil(0).NextDouble;

[Fact]
public void IsValidDelay_Ok()
Expand All @@ -26,7 +26,7 @@ public void UnsupportedRetryBackoffType_Throws()
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
double state = 0;
return RetryHelper.GetRetryDelay(type, 0, TimeSpan.FromSeconds(1), ref state, _randomUtil);
return RetryHelper.GetRetryDelay(type, 0, TimeSpan.FromSeconds(1), ref state, _randomizer);
});
}

Expand All @@ -35,41 +35,41 @@ public void Constant_Ok()
{
double state = 0;

RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 0, TimeSpan.Zero, ref state, _randomUtil).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 1, TimeSpan.Zero, ref state, _randomUtil).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 2, TimeSpan.Zero, ref state, _randomUtil).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);

RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 0, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 1, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 2, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(1));
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));
}

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

RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 0, TimeSpan.Zero, ref state, _randomUtil).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 1, TimeSpan.Zero, ref state, _randomUtil).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 2, TimeSpan.Zero, ref state, _randomUtil).Should().Be(TimeSpan.Zero);
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, 0, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 1, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(2));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 2, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(3));
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));
}

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

RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 0, TimeSpan.Zero, ref state, _randomUtil).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 1, TimeSpan.Zero, ref state, _randomUtil).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 2, TimeSpan.Zero, ref state, _randomUtil).Should().Be(TimeSpan.Zero);
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, 0, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 1, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(2));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 2, TimeSpan.FromSeconds(1), ref state, _randomUtil).Should().Be(TimeSpan.FromSeconds(4));
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));
}

[InlineData(1)]
Expand All @@ -94,20 +94,20 @@ public void ExponentialWithJitter_Ok(int count)
public void ExponentialWithJitter_EnsureRandomness()
{
var delay = TimeSpan.FromSeconds(7.8);
var delays1 = GetExponentialWithJitterBackoff(false, delay, 100, RandomUtil.Instance);
var delays2 = GetExponentialWithJitterBackoff(false, delay, 100, RandomUtil.Instance);
var delays1 = GetExponentialWithJitterBackoff(false, delay, 100, RandomUtil.Instance.NextDouble);
var delays2 = GetExponentialWithJitterBackoff(false, delay, 100, RandomUtil.Instance.NextDouble);

delays1.SequenceEqual(delays2).Should().BeFalse();
}

private static IReadOnlyList<TimeSpan> GetExponentialWithJitterBackoff(bool contrib, TimeSpan baseDelay, int retryCount, RandomUtil? util = null)
private static IReadOnlyList<TimeSpan> GetExponentialWithJitterBackoff(bool contrib, TimeSpan baseDelay, int retryCount, Func<double>? randomizer = null)
{
if (contrib)
{
return Backoff.DecorrelatedJitterBackoffV2(baseDelay, retryCount, 0, false).Take(retryCount).ToArray();
}

var random = util ?? new RandomUtil(0);
var random = randomizer ?? new RandomUtil(0).NextDouble;
double state = 0;
var result = new List<TimeSpan>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,48 @@ public void AddRetry_InvalidOptions_Throws()
.Should()
.Throw<ValidationException>();
}

[Fact]
public void GetAggregatedDelay_ShouldReturnTheSameValue()
{
var options = new RetryStrategyOptions { BackoffType = RetryBackoffType.ExponentialWithJitter };

var delay = GetAggregatedDelay(options);
GetAggregatedDelay(options).Should().Be(delay);
}

[Fact]
public void GetAggregatedDelay_EnsureCorrectValue()
{
var options = new RetryStrategyOptions { BackoffType = RetryBackoffType.Constant, BaseDelay = TimeSpan.FromSeconds(1), RetryCount = 5 };

GetAggregatedDelay(options).Should().Be(TimeSpan.FromSeconds(5));
}

private static TimeSpan GetAggregatedDelay<T>(RetryStrategyOptions<T> options)
{
var aggregatedDelay = TimeSpan.Zero;

var strategy = new ResilienceStrategyBuilder { Randomizer = () => 1.0 }.AddRetry(new()
{
RetryCount = options.RetryCount,
BaseDelay = options.BaseDelay,
BackoffType = options.BackoffType,
ShouldHandle = _ => PredicateResult.True, // always retry until all retries are exhausted
RetryDelayGenerator = args =>
{
// the delay hint is calculated for this attempt by the retry strategy
aggregatedDelay += args.Arguments.DelayHint;
// return zero delay, so no waiting
return new ValueTask<TimeSpan>(TimeSpan.Zero);
}
})
.Build();

// this executes all retries and we aggregate the delays immediately
strategy.Execute(() => { });

return aggregatedDelay;
}
}
Loading

0 comments on commit b60eea8

Please sign in to comment.