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

Introduce RetryResilienceStrategy #1101

Merged
merged 6 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
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