Skip to content

Commit

Permalink
Introduce RetryResilienceStrategy (#1101)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Apr 11, 2023
1 parent 40606d2 commit 2627c45
Show file tree
Hide file tree
Showing 28 changed files with 886 additions and 31 deletions.
18 changes: 18 additions & 0 deletions src/Polly.Core.Tests/Retry/RetryConstantsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Polly.Retry;

namespace Polly.Core.Tests.Retry;

public class RetryConstantsTests
{
[Fact]
public void EnsureDefaults()
{
RetryConstants.DefaultBackoffType.Should().Be(RetryBackoffType.Exponential);
RetryConstants.DefaultBaseDelay.Should().Be(TimeSpan.FromSeconds(2));
RetryConstants.DefaultRetryCount.Should().Be(3);
RetryConstants.MaxRetryCount.Should().Be(100);
RetryConstants.InfiniteRetryCount.Should().Be(-1);
RetryConstants.StrategyType.Should().Be("Retry");
RetryConstants.OnRetryEvent.Should().Be("OnRetry");
}
}
3 changes: 2 additions & 1 deletion src/Polly.Core.Tests/Retry/RetryDelayArgumentsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ public class RetryDelayArgumentsTests
[Fact]
public void Ctor_Ok()
{
var args = new RetryDelayArguments(ResilienceContext.Get(), 2);
var args = new RetryDelayArguments(ResilienceContext.Get(), 2, TimeSpan.FromSeconds(2));

args.Context.Should().NotBeNull();
args.Attempt.Should().Be(2);
args.DelayHint.Should().Be(TimeSpan.FromSeconds(2));
}
}
4 changes: 2 additions & 2 deletions src/Polly.Core.Tests/Retry/RetryDelayGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public async Task NoGeneratorRegisteredForType_EnsureDefaultValue()
var result = await new RetryDelayGenerator()
.SetGenerator<int>((_, _) => TimeSpan.Zero)
.CreateHandler()!
.Generate(new Outcome<bool>(true), new RetryDelayArguments(ResilienceContext.Get(), 0));
.Generate(new Outcome<bool>(true), new RetryDelayArguments(ResilienceContext.Get(), 0, TimeSpan.FromSeconds(2)));

result.Should().Be(TimeSpan.MinValue);
}
Expand All @@ -28,7 +28,7 @@ public async Task GeneratorRegistered_EnsureValueNotIgnored(TimeSpan delay)
var result = await new RetryDelayGenerator()
.SetGenerator<int>((_, _) => delay)
.CreateHandler()!
.Generate(new Outcome<int>(0), new RetryDelayArguments(ResilienceContext.Get(), 0));
.Generate(new Outcome<int>(0), new RetryDelayArguments(ResilienceContext.Get(), 0, TimeSpan.FromSeconds(2)));

result.Should().Be(delay);
}
Expand Down
61 changes: 61 additions & 0 deletions src/Polly.Core.Tests/Retry/RetryHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using Polly.Retry;

namespace Polly.Core.Tests.Retry;

public class RetryHelperTests
{
[Fact]
public void IsValidDelay_Ok()
{
RetryHelper.IsValidDelay(TimeSpan.Zero).Should().BeTrue();
RetryHelper.IsValidDelay(TimeSpan.FromSeconds(1)).Should().BeTrue();
RetryHelper.IsValidDelay(TimeSpan.MaxValue).Should().BeTrue();
RetryHelper.IsValidDelay(TimeSpan.MinValue).Should().BeFalse();
RetryHelper.IsValidDelay(TimeSpan.FromMilliseconds(-1)).Should().BeFalse();
}

[Fact]
public void UnsupportedRetryBackoffType_Throws()
{
RetryBackoffType type = (RetryBackoffType)99;

Assert.Throws<ArgumentOutOfRangeException>(() => RetryHelper.GetRetryDelay(type, 0, TimeSpan.FromSeconds(1)));
}

[Fact]
public void Constant_Ok()
{
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 0, TimeSpan.Zero).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 1, TimeSpan.Zero).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 2, TimeSpan.Zero).Should().Be(TimeSpan.Zero);

RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 0, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 1, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Constant, 2, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(1));
}

[Fact]
public void Linear_Ok()
{
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 0, TimeSpan.Zero).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 1, TimeSpan.Zero).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 2, TimeSpan.Zero).Should().Be(TimeSpan.Zero);

RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 0, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 1, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(2));
RetryHelper.GetRetryDelay(RetryBackoffType.Linear, 2, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(3));
}

[Fact]
public void Exponential_Ok()
{
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 0, TimeSpan.Zero).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 1, TimeSpan.Zero).Should().Be(TimeSpan.Zero);
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 2, TimeSpan.Zero).Should().Be(TimeSpan.Zero);

RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 0, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(1));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 1, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(2));
RetryHelper.GetRetryDelay(RetryBackoffType.Exponential, 2, TimeSpan.FromSeconds(1)).Should().Be(TimeSpan.FromSeconds(4));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.ComponentModel.DataAnnotations;
using Polly.Builder;
using Polly.Retry;
using Xunit;

namespace Polly.Core.Tests.Retry;

public class RetryResilienceStrategyBuilderExtensionsTests
{
public static readonly TheoryData<Action<ResilienceStrategyBuilder>> OverloadsData = new()
{
builder =>
{
builder.AddRetry(retry=>retry.Result(10));
AssertStrategy(builder, RetryBackoffType.Exponential, 3, TimeSpan.FromSeconds(2));
},
builder =>
{
builder.AddRetry(retry=>retry.Result(10), RetryBackoffType.Linear);
AssertStrategy(builder, RetryBackoffType.Linear, 3, TimeSpan.FromSeconds(2));
},
builder =>
{
builder.AddRetry(retry=>retry.Result(10), RetryBackoffType.Linear, 2, TimeSpan.FromSeconds(1));
AssertStrategy(builder, RetryBackoffType.Linear, 2, TimeSpan.FromSeconds(1));
},
};

[MemberData(nameof(OverloadsData))]
[Theory]
public void AddRetry_Overloads_Ok(Action<ResilienceStrategyBuilder> configure)
{
var builder = new ResilienceStrategyBuilder();
var options = new RetryStrategyOptions();

builder.Invoking(b => configure(b)).Should().NotThrow();
}

[Fact]
public void AddRetry_DefaultOptions_Ok()
{
var builder = new ResilienceStrategyBuilder();
var options = new RetryStrategyOptions();

builder.AddRetry(options);

AssertStrategy(builder, options.BackoffType, options.RetryCount, options.BaseDelay);
}

private static void AssertStrategy(ResilienceStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay)
{
var strategy = (RetryResilienceStrategy)builder.Build();

strategy.BackoffType.Should().Be(type);
strategy.RetryCount.Should().Be(retries);
strategy.BaseDelay.Should().Be(delay);
}

[Fact]
public void AddRetry_InvalidOptions_Throws()
{
var builder = new ResilienceStrategyBuilder();

builder
.Invoking(b => b.AddRetry(new RetryStrategyOptions { ShouldRetry = null! }))
.Should()
.Throw<ValidationException>()
.WithMessage("The retry strategy options are invalid.*");
}
}
176 changes: 176 additions & 0 deletions src/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using Moq;
using Polly.Retry;
using Polly.Telemetry;

namespace Polly.Core.Tests.Retry;

public class RetryResilienceStrategyTests
{
private readonly RetryStrategyOptions _options = new();
private readonly FakeTimeProvider _timeProvider = new();
private readonly Mock<ResilienceTelemetry> _telemetry = new();

[Fact]
public void ShouldRetryEmpty_Skipped()
{
bool called = false;
_options.OnRetry.Add<int>(() => called = true);
SetupNoDelay();
var sut = CreateSut();

sut.Execute(_ => 0, default);

called.Should().BeFalse();
}

[Fact]
public void Retry_RetryCount_Respected()
{
int calls = 0;
_options.OnRetry.Add<int>(() => calls++);
_options.ShouldRetry.Result<int>(0);
_options.RetryCount = 12;
SetupNoDelay();
var sut = CreateSut();

sut.Execute(_ => 0, default);

calls.Should().Be(12);
}

[Fact]
public void RetryException_RetryCount_Respected()
{
int calls = 0;
_options.OnRetry.Add<int>((args, _) =>
{
args.Exception.Should().BeOfType<InvalidOperationException>();
calls++;
});
_options.ShouldRetry.Exception<InvalidOperationException>();
_options.RetryCount = 3;
SetupNoDelay();
var sut = CreateSut();

Assert.Throws<InvalidOperationException>(() => sut.Execute<int>(_ => throw new InvalidOperationException(), default));

calls.Should().Be(3);
}

[Fact]
public void Retry_Infinite_Respected()
{
int calls = 0;
_options.BackoffType = RetryBackoffType.Constant;
_options.OnRetry.Add<int>((_, args) =>
{
if (args.Attempt > RetryConstants.MaxRetryCount)
{
throw new InvalidOperationException();
}
calls++;
});
_options.ShouldRetry.Result(0);
_options.RetryCount = RetryStrategyOptions.InfiniteRetryCount;
SetupNoDelay();
var sut = CreateSut();

Assert.Throws<InvalidOperationException>(() => sut.Execute(_ => 0, default));

calls.Should().Be(RetryConstants.MaxRetryCount + 1);
}

[Fact]
public void RetryDelayGenerator_Respected()
{
int calls = 0;
_options.OnRetry.Add<int>(() => calls++);
_options.ShouldRetry.Result<int>(0);
_options.RetryCount = 3;
_options.BackoffType = RetryBackoffType.Constant;
_options.RetryDelayGenerator.SetGenerator<int>((_, _) => TimeSpan.FromMilliseconds(123));
_timeProvider.SetupDelay(TimeSpan.FromMilliseconds(123));

var sut = CreateSut();

sut.Execute(_ => 0, default);

_timeProvider.Verify(v => v.Delay(TimeSpan.FromMilliseconds(123), default), Times.Exactly(3));
}

[Fact]
public void OnRetry_EnsureCorrectArguments()
{
var attempts = new List<int>();
var delays = new List<TimeSpan>();
_options.OnRetry.Add<int>((outcome, args) =>
{
attempts.Add(args.Attempt);
delays.Add(args.RetryDelay);
outcome.Exception.Should().BeNull();
outcome.Result.Should().Be(0);
});

_options.ShouldRetry.Result<int>(0);
_options.RetryCount = 3;
_options.BackoffType = RetryBackoffType.Linear;
_timeProvider.SetupAnyDelay();

var sut = CreateSut();

sut.Execute(_ => 0, default);

attempts.Should().HaveCount(3);
attempts[0].Should().Be(0);
attempts[1].Should().Be(1);
attempts[2].Should().Be(2);

delays[0].Should().Be(TimeSpan.FromSeconds(2));
delays[1].Should().Be(TimeSpan.FromSeconds(4));
delays[2].Should().Be(TimeSpan.FromSeconds(6));
}

[Fact]
public void RetryDelayGenerator_EnsureCorrectArguments()
{
var attempts = new List<int>();
var hints = new List<TimeSpan>();
_options.RetryDelayGenerator.SetGenerator<int>((outcome, args) =>
{
attempts.Add(args.Attempt);
hints.Add(args.DelayHint);
outcome.Exception.Should().BeNull();
outcome.Result.Should().Be(0);
return TimeSpan.Zero;
});

_options.ShouldRetry.Result<int>(0);
_options.RetryCount = 3;
_options.BackoffType = RetryBackoffType.Linear;
_timeProvider.SetupAnyDelay();

var sut = CreateSut();

sut.Execute(_ => 0, default);

attempts.Should().HaveCount(3);
attempts[0].Should().Be(0);
attempts[1].Should().Be(1);
attempts[2].Should().Be(2);

hints[0].Should().Be(TimeSpan.FromSeconds(2));
hints[1].Should().Be(TimeSpan.FromSeconds(4));
hints[2].Should().Be(TimeSpan.FromSeconds(6));
}

private void SetupNoDelay() => _options.RetryDelayGenerator.SetGenerator<int>((_, _) => TimeSpan.Zero);

private RetryResilienceStrategy CreateSut()
{
return new RetryResilienceStrategy(_options, _timeProvider.Object, _telemetry.Object);
}
}
Loading

0 comments on commit 2627c45

Please sign in to comment.