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 OutcomeGenerator #1095

Merged
merged 3 commits into from
Apr 3, 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
2 changes: 1 addition & 1 deletion src/Polly.Core.Tests/Polly.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Nullable>enable</Nullable>
<SkipPollyUsings>true</SkipPollyUsings>
<Threshold>100</Threshold>
<NoWarn>$(NoWarn);SA1600;SA1204</NoWarn>
<NoWarn>$(NoWarn);SA1600;SA1204;SA1602</NoWarn>
<Include>[Polly.Core]*</Include>
</PropertyGroup>

Expand Down
15 changes: 15 additions & 0 deletions src/Polly.Core.Tests/Retry/RetryDelayArgumentsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Polly.Retry;

namespace Polly.Core.Tests.Retry;

public class RetryDelayArgumentsTests
{
[Fact]
public void Ctor_Ok()
{
var args = new RetryDelayArguments(ResilienceContext.Get(), 2);

args.Context.Should().NotBeNull();
args.Attempt.Should().Be(2);
}
}
35 changes: 35 additions & 0 deletions src/Polly.Core.Tests/Retry/RetryDelayGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Threading.Tasks;
using Polly.Retry;
using Polly.Strategy;
using Xunit;

namespace Polly.Core.Tests.Retry;

public class RetryDelayGeneratorTests
{
[Fact]
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));

result.Should().Be(TimeSpan.MinValue);
}

public static readonly TheoryData<TimeSpan> ValidDelays = new() { TimeSpan.Zero, TimeSpan.FromMilliseconds(123) };

[MemberData(nameof(ValidDelays))]
[Theory]
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));

result.Should().Be(delay);
}
}
3 changes: 3 additions & 0 deletions src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ public void Ctor_Ok()

options.ShouldRetry.Should().NotBeNull();
options.ShouldRetry.IsEmpty.Should().BeTrue();

options.RetryDelayGenerator.Should().NotBeNull();
options.RetryDelayGenerator.IsEmpty.Should().BeTrue();
}
}
148 changes: 148 additions & 0 deletions src/Polly.Core.Tests/Strategy/OutcomeGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System;
using System.Threading.Tasks;
using Polly.Strategy;

namespace Polly.Core.Tests.Strategy;

public class OutcomeGeneratorTests
{
private readonly DummyGenerator _sut = new();

[Fact]
public void Empty_Ok()
{
_sut.IsEmpty.Should().BeTrue();

_sut.SetGenerator<int>((_, _) => GeneratedValue.Invalid);

_sut.IsEmpty.Should().BeFalse();
}

[Fact]
public void CreateHandler_Empty_ReturnsNull()
{
_sut.CreateHandler().Should().BeNull();
}

public static readonly TheoryData<Action<DummyGenerator>> Data = new()
{
sut =>
{
sut.SetGenerator<int>((_, _) => GeneratedValue.Valid1);
InvokeHandler(sut, new Outcome<int>(0), GeneratedValue.Valid1);
},
sut =>
{
sut.SetGenerator<int>((_, _) => new ValueTask<GeneratedValue>(GeneratedValue.Valid1));
InvokeHandler(sut, new Outcome<int>(0), GeneratedValue.Valid1);
},
sut =>
{
sut.SetGenerator<int>((_, _) => GeneratedValue.Valid1);
InvokeHandler(sut, new Outcome<bool>(true), GeneratedValue.Default);
},
sut =>
{
sut.SetGenerator<int>((_, _) => new ValueTask<GeneratedValue>(GeneratedValue.Valid1));
InvokeHandler(sut, new Outcome<bool>(true), GeneratedValue.Default);
},
sut =>
{
sut.SetGenerator<int>((_, _) => GeneratedValue.Invalid);
InvokeHandler(sut, new Outcome<int>(0), GeneratedValue.Default);
},
sut =>
{
sut.SetGenerator<int>((_, _) => new ValueTask<GeneratedValue>(GeneratedValue.Invalid));
InvokeHandler(sut, new Outcome<int>(0), GeneratedValue.Default);
},
sut =>
{
sut.SetGenerator<int>((_, _) => GeneratedValue.Valid1);
sut.SetGenerator<int>((_, _) => GeneratedValue.Valid2);
InvokeHandler(sut, new Outcome<int>(0), GeneratedValue.Valid2);
},
sut =>
{
sut.SetGenerator<int>((_, _) => new ValueTask<GeneratedValue>(GeneratedValue.Valid1));
sut.SetGenerator<int>((_, _) => new ValueTask<GeneratedValue>(GeneratedValue.Valid2));
InvokeHandler(sut, new Outcome<int>(0), GeneratedValue.Valid2);
},
sut =>
{
sut.SetGenerator<int>((_, _) => GeneratedValue.Valid1);
sut.SetGenerator<bool>((_, _) => GeneratedValue.Valid2);
InvokeHandler(sut, new Outcome<int>(0), GeneratedValue.Valid1);
InvokeHandler(sut, new Outcome<bool>(true), GeneratedValue.Valid2);
},
sut =>
{
sut.SetGenerator<int>((_, _) => GeneratedValue.Valid1);
sut.SetGenerator<double>((_, _) => GeneratedValue.Valid1);
InvokeHandler(sut, new Outcome<bool>(true), GeneratedValue.Default);
},
};

[MemberData(nameof(Data))]
[Theory]
public void ResultHandler_SinglePredicate_Ok(Action<DummyGenerator> callback)
{
_sut.Invoking(s => callback(s)).Should().NotThrow();
callback(_sut);
}

[Fact]
public void AddResultHandlers_DifferentResultType_NotInvoked()
{
var callbacks = new List<int>();

for (var i = 0; i < 10; i++)
{
var index = i;

_sut.SetGenerator<int>((_, _) =>
{
callbacks.Add(index);
return GeneratedValue.Valid1;
});

_sut.SetGenerator<bool>((_, _) =>
{
callbacks.Add(index);
return GeneratedValue.Valid1;
});
}

InvokeHandler(_sut, new Outcome<int>(1), GeneratedValue.Valid1);

callbacks.Distinct().Should().HaveCount(1);
}

private static void InvokeHandler<T>(DummyGenerator sut, Outcome<T> outcome, GeneratedValue expectedResult)
{
var args = new Args();
sut.CreateHandler()!.Generate(outcome, args).AsTask().Result.Should().Be(expectedResult);
}

public sealed class DummyGenerator : OutcomeGenerator<GeneratedValue, Args, DummyGenerator>
{
protected override GeneratedValue DefaultValue => GeneratedValue.Default;

protected override bool IsValid(GeneratedValue value) => value == GeneratedValue.Valid1 || value == GeneratedValue.Valid2;
}

public enum GeneratedValue
{
Default,
Valid1,
Valid2,
Invalid
}

public class Args : IResilienceArguments
{
public Args() => Context = ResilienceContext.Get();

public ResilienceContext Context { get; private set; }
}
}
28 changes: 28 additions & 0 deletions src/Polly.Core/Retry/RetryDelayArguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Polly.Strategy;

namespace Polly.Retry;

#pragma warning disable CA1815 // Override equals and operator equals on value types

/// <summary>
/// Represents the arguments used in <see cref="RetryDelayGenerator"/> for generating the next retry delay.
/// </summary>
public readonly struct RetryDelayArguments : IResilienceArguments
{
internal RetryDelayArguments(ResilienceContext context, int attempt)
{
Attempt = attempt;
Context = context;
}

/// <summary>
/// Gets the zero-based attempt number.
/// </summary>
/// <remarks>
/// The first attempt is 0, the second attempt is 1, and so on.
/// </remarks>
public int Attempt { get; }

/// <inheritdoc/>
public ResilienceContext Context { get; }
}
18 changes: 18 additions & 0 deletions src/Polly.Core/Retry/RetryDelayGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Polly.Strategy;

namespace Polly.Retry;

/// <summary>
/// This class generates the customized retries used in retry strategy.
/// </summary>
/// <remarks>
/// If the generator returns a negative value, it's value is ignored.
/// </remarks>
public sealed class RetryDelayGenerator : OutcomeGenerator<TimeSpan, RetryDelayArguments, RetryDelayGenerator>
{
/// <inheritdoc/>
protected override TimeSpan DefaultValue => TimeSpan.MinValue;

/// <inheritdoc/>
protected override bool IsValid(TimeSpan value) => value >= TimeSpan.Zero;
}
12 changes: 12 additions & 0 deletions src/Polly.Core/Retry/RetryStrategyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ public class RetryStrategyOptions
/// <summary>
/// Gets or sets the <see cref="ShouldRetryPredicate"/> instance used to determine if a retry should be performed.
/// </summary>
/// <remarks>
/// By default, the predicate is empty and no results or exceptions are retried.
/// </remarks>
[Required]
public ShouldRetryPredicate ShouldRetry { get; set; } = new();

/// <summary>
/// Gets or sets the <see cref="RetryDelayGenerator"/> instance that is used to generated the delay between retries.
/// </summary>
/// <remarks>
/// By default, the generator is empty and it does not affect the delay between retries.
/// </remarks>
[Required]
public RetryDelayGenerator RetryDelayGenerator { get; set; } = new();
}
91 changes: 91 additions & 0 deletions src/Polly.Core/Strategy/OutcomeGenerator.Handler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System.Collections.Generic;

namespace Polly.Strategy;

#pragma warning disable CA1034 // Nested types should not be visible
#pragma warning disable CA1005 // Avoid excessive parameters on generic types
#pragma warning disable S2436 // Types and methods should not have too many generic parameters

public abstract partial class OutcomeGenerator<TGeneratedValue, TArgs, TSelf>
{
/// <summary>
/// The resulting handler for the outcome.
/// </summary>
public abstract class Handler
{
private protected Handler(TGeneratedValue defaultValue, Predicate<TGeneratedValue> isValid)
{
DefaultValue = defaultValue;
IsValid = isValid;
}

internal TGeneratedValue DefaultValue { get; }

internal Predicate<TGeneratedValue> IsValid { get; }

/// <summary>
/// Determines if the handler should handle the outcome.
/// </summary>
/// <typeparam name="TResult">The result type to add a predicate for.</typeparam>
/// <param name="outcome">The operation outcome.</param>
/// <param name="args">The arguments.</param>
/// <returns>The result of the handle operation.</returns>
public abstract ValueTask<TGeneratedValue> Generate<TResult>(Outcome<TResult> outcome, TArgs args);
}

private sealed class TypeHandler : Handler
{
private readonly Type _type;
private readonly object _generator;

public TypeHandler(
Type type,
object generator,
TGeneratedValue defaultValue,
Predicate<TGeneratedValue> isValid)
: base(defaultValue, isValid)
{
_type = type;
_generator = generator;
}

public override async ValueTask<TGeneratedValue> Generate<TResult>(Outcome<TResult> outcome, TArgs args)
{
if (typeof(TResult) == _type)
martincostello marked this conversation as resolved.
Show resolved Hide resolved
{
var value = await ((Func<Outcome<TResult>, TArgs, ValueTask<TGeneratedValue>>)_generator)(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext);

if (IsValid(value))
{
return value;
}

return DefaultValue;
}

return DefaultValue;
}
}

private sealed class TypesHandler : Handler
{
private readonly Dictionary<Type, TypeHandler> _generators;

public TypesHandler(
IEnumerable<KeyValuePair<Type, object>> generators,
TGeneratedValue defaultValue,
Predicate<TGeneratedValue> isValid)
: base(defaultValue, isValid)
=> _generators = generators.ToDictionary(v => v.Key, v => new TypeHandler(v.Key, v.Value, defaultValue, isValid));

public override ValueTask<TGeneratedValue> Generate<TResult>(Outcome<TResult> outcome, TArgs args)
{
if (_generators.TryGetValue(typeof(TResult), out var handler))
{
return handler.Generate(outcome, args);
}

return new ValueTask<TGeneratedValue>(DefaultValue);
}
}
}
Loading