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

Expose Randomizer property and use it in retry strategy #1346

Merged
merged 1 commit into from
Jun 23, 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
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