Skip to content

Commit

Permalink
Introduce OutcomeGenerator (#1095)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Apr 3, 2023
1 parent 1241a4a commit b1ffcb9
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 1 deletion.
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)
{
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

0 comments on commit b1ffcb9

Please sign in to comment.