Skip to content

Commit

Permalink
Introduce FaultGenerator and OutcomeGenerator<T> (#1911)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Jan 21, 2024
1 parent a1440fe commit cf00835
Show file tree
Hide file tree
Showing 10 changed files with 475 additions and 29 deletions.
16 changes: 14 additions & 2 deletions src/Polly.Core/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Polly.Simmy.EnabledGeneratorArguments
Polly.Simmy.EnabledGeneratorArguments.Context.get -> Polly.ResilienceContext!
Polly.Simmy.EnabledGeneratorArguments.EnabledGeneratorArguments() -> void
Polly.Simmy.EnabledGeneratorArguments.EnabledGeneratorArguments(Polly.ResilienceContext! context) -> void
Polly.Simmy.Fault.FaultGenerator
Polly.Simmy.Fault.FaultGenerator.AddException(System.Func<Polly.ResilienceContext!, System.Exception!>! generator, int weight = 100) -> Polly.Simmy.Fault.FaultGenerator!
Polly.Simmy.Fault.FaultGenerator.AddException(System.Func<System.Exception!>! generator, int weight = 100) -> Polly.Simmy.Fault.FaultGenerator!
Polly.Simmy.Fault.FaultGenerator.AddException<TException>(int weight = 100) -> Polly.Simmy.Fault.FaultGenerator!
Polly.Simmy.Fault.FaultGenerator.FaultGenerator() -> void
Polly.Simmy.Fault.FaultGeneratorArguments
Polly.Simmy.Fault.FaultGeneratorArguments.Context.get -> Polly.ResilienceContext!
Polly.Simmy.Fault.FaultGeneratorArguments.FaultGeneratorArguments() -> void
Expand Down Expand Up @@ -84,12 +89,17 @@ Polly.Simmy.Outcomes.OnOutcomeInjectedArguments<TResult>.Context.get -> Polly.Re
Polly.Simmy.Outcomes.OnOutcomeInjectedArguments<TResult>.OnOutcomeInjectedArguments() -> void
Polly.Simmy.Outcomes.OnOutcomeInjectedArguments<TResult>.OnOutcomeInjectedArguments(Polly.ResilienceContext! context, Polly.Outcome<TResult> outcome) -> void
Polly.Simmy.Outcomes.OnOutcomeInjectedArguments<TResult>.Outcome.get -> Polly.Outcome<TResult>
Polly.Simmy.Outcomes.OutcomeGenerator<TResult>
Polly.Simmy.Outcomes.OutcomeGenerator<TResult>.AddException(System.Func<Polly.ResilienceContext!, System.Exception!>! generator, int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator<TResult>!
Polly.Simmy.Outcomes.OutcomeGenerator<TResult>.AddException(System.Func<System.Exception!>! generator, int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator<TResult>!
Polly.Simmy.Outcomes.OutcomeGenerator<TResult>.AddException<TException>(int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator<TResult>!
Polly.Simmy.Outcomes.OutcomeGenerator<TResult>.AddResult(System.Func<Polly.ResilienceContext!, TResult>! generator, int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator<TResult>!
Polly.Simmy.Outcomes.OutcomeGenerator<TResult>.AddResult(System.Func<TResult>! generator, int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator<TResult>!
Polly.Simmy.Outcomes.OutcomeGenerator<TResult>.OutcomeGenerator() -> void
Polly.Simmy.Outcomes.OutcomeGeneratorArguments
Polly.Simmy.Outcomes.OutcomeGeneratorArguments.Context.get -> Polly.ResilienceContext!
Polly.Simmy.Outcomes.OutcomeGeneratorArguments.OutcomeGeneratorArguments() -> void
Polly.Simmy.Outcomes.OutcomeGeneratorArguments.OutcomeGeneratorArguments(Polly.ResilienceContext! context) -> void
Polly.Simmy.Outcomes.OutcomeStrategyOptions
Polly.Simmy.Outcomes.OutcomeStrategyOptions.OutcomeStrategyOptions() -> void
Polly.Simmy.Outcomes.OutcomeStrategyOptions<TResult>
Polly.Simmy.Outcomes.OutcomeStrategyOptions<TResult>.OnOutcomeInjected.get -> System.Func<Polly.Simmy.Outcomes.OnOutcomeInjectedArguments<TResult>, System.Threading.Tasks.ValueTask>?
Polly.Simmy.Outcomes.OutcomeStrategyOptions<TResult>.OnOutcomeInjected.set -> void
Expand All @@ -98,9 +108,11 @@ Polly.Simmy.Outcomes.OutcomeStrategyOptions<TResult>.OutcomeGenerator.set -> voi
Polly.Simmy.Outcomes.OutcomeStrategyOptions<TResult>.OutcomeStrategyOptions() -> void
static Polly.Simmy.BehaviorPipelineBuilderExtensions.AddChaosBehavior<TBuilder>(this TBuilder! builder, double injectionRate, System.Func<System.Threading.Tasks.ValueTask>! behavior) -> TBuilder!
static Polly.Simmy.BehaviorPipelineBuilderExtensions.AddChaosBehavior<TBuilder>(this TBuilder! builder, Polly.Simmy.Behavior.BehaviorStrategyOptions! options) -> TBuilder!
static Polly.Simmy.Fault.FaultGenerator.implicit operator System.Func<Polly.Simmy.Fault.FaultGeneratorArguments, System.Threading.Tasks.ValueTask<System.Exception?>>!(Polly.Simmy.Fault.FaultGenerator! generator) -> System.Func<Polly.Simmy.Fault.FaultGeneratorArguments, System.Threading.Tasks.ValueTask<System.Exception?>>!
static Polly.Simmy.FaultPipelineBuilderExtensions.AddChaosFault<TBuilder>(this TBuilder! builder, double injectionRate, System.Func<System.Exception?>! faultGenerator) -> TBuilder!
static Polly.Simmy.FaultPipelineBuilderExtensions.AddChaosFault<TBuilder>(this TBuilder! builder, Polly.Simmy.Fault.FaultStrategyOptions! options) -> TBuilder!
static Polly.Simmy.LatencyPipelineBuilderExtensions.AddChaosLatency<TBuilder>(this TBuilder! builder, double injectionRate, System.TimeSpan latency) -> TBuilder!
static Polly.Simmy.LatencyPipelineBuilderExtensions.AddChaosLatency<TBuilder>(this TBuilder! builder, Polly.Simmy.Latency.LatencyStrategyOptions! options) -> TBuilder!
static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosResult<TResult>(this Polly.ResiliencePipelineBuilder<TResult>! builder, double injectionRate, System.Func<TResult?>! resultGenerator) -> Polly.ResiliencePipelineBuilder<TResult>!
static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosResult<TResult>(this Polly.ResiliencePipelineBuilder<TResult>! builder, Polly.Simmy.Outcomes.OutcomeStrategyOptions<TResult>! options) -> Polly.ResiliencePipelineBuilder<TResult>!
static Polly.Simmy.Outcomes.OutcomeGenerator<TResult>.implicit operator System.Func<Polly.Simmy.Outcomes.OutcomeGeneratorArguments, System.Threading.Tasks.ValueTask<Polly.Outcome<TResult>?>>!(Polly.Simmy.Outcomes.OutcomeGenerator<TResult>! generator) -> System.Func<Polly.Simmy.Outcomes.OutcomeGeneratorArguments, System.Threading.Tasks.ValueTask<Polly.Outcome<TResult>?>>!
84 changes: 84 additions & 0 deletions src/Polly.Core/Simmy/Fault/FaultGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.ComponentModel;
using Polly.Simmy.Utils;

namespace Polly.Simmy.Fault;

#pragma warning disable CA2225 // Operator overloads have named alternates
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters

/// <summary>
/// A generator for creating faults (exceptions) using registered delegate functions.
/// </summary>
/// <remarks>
/// An instance of this class can be assigned to the <see cref="FaultStrategyOptions.FaultGenerator"/> property.
/// </remarks>
public sealed class FaultGenerator
{
private const int DefaultWeight = 100;

private readonly GeneratorHelper<VoidResult> _helper;

/// <summary>
/// Initializes a new instance of the <see cref="FaultGenerator"/> class.
/// </summary>
public FaultGenerator()
=> _helper = new GeneratorHelper<VoidResult>(RandomUtil.Instance.Next);

/// <summary>
/// Registers an exception generator delegate.
/// </summary>
/// <param name="generator">The delegate that generates the exception.</param>
/// <param name="weight">The weight assigned to this generator. Defaults to <c>100</c>.</param>
/// <returns>The current instance of <see cref="FaultGenerator"/>.</returns>
public FaultGenerator AddException(Func<Exception> generator, int weight = DefaultWeight)
{
Guard.NotNull(generator);

_helper.AddOutcome(_ => Outcome.FromException<VoidResult>(generator()), weight);

return this;
}

/// <summary>
/// Registers an exception generator delegate that accepts a <see cref="ResilienceContext"/>.
/// </summary>
/// <param name="generator">The delegate that generates the exception, accepting a <see cref="ResilienceContext"/>.</param>
/// <param name="weight">The weight assigned to this generator. Defaults to <c>100</c>.</param>
/// <returns>The current instance of <see cref="FaultGenerator"/>.</returns>
public FaultGenerator AddException(Func<ResilienceContext, Exception> generator, int weight = DefaultWeight)
{
Guard.NotNull(generator);

_helper.AddOutcome(context => Outcome.FromException<VoidResult>(generator(context)), weight);

return this;
}

/// <summary>
/// Registers an exception generator for a specific exception type, using the default constructor of that exception.
/// </summary>
/// <typeparam name="TException">The type of the exception to generate.</typeparam>
/// <param name="weight">The weight assigned to this generator. Defaults to <c>100</c>.</param>
/// <returns>The current instance of <see cref="FaultGenerator"/>.</returns>
public FaultGenerator AddException<TException>(int weight = DefaultWeight)
where TException : Exception, new()
{
_helper.AddOutcome(_ => Outcome.FromException<VoidResult>(new TException()), weight);

return this;
}

/// <summary>
/// Provides an implicit conversion from <see cref="FaultGenerator"/> to a delegate compatible with <see cref="FaultStrategyOptions.FaultGenerator"/>.
/// </summary>
/// <param name="generator">The instance of <see cref="FaultGenerator"/>.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator Func<FaultGeneratorArguments, ValueTask<Exception?>>(FaultGenerator generator)
{
Guard.NotNull(generator);

var generatorDelegate = generator._helper.CreateGenerator();

return args => new ValueTask<Exception?>(generatorDelegate(args.Context)!.Value.Exception);
}
}
120 changes: 120 additions & 0 deletions src/Polly.Core/Simmy/Outcomes/OutcomeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.ComponentModel;
using Polly.Simmy.Utils;

namespace Polly.Simmy.Outcomes;

#pragma warning disable CA2225 // Operator overloads have named alternates
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters

/// <summary>
/// Generator that produces faults such as exceptions or results.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <remarks>
/// An instance of this class is assignable to <see cref="OutcomeStrategyOptions{TResult}.OutcomeGenerator"/>.
/// </remarks>
public sealed class OutcomeGenerator<TResult>
{
private const int DefaultWeight = 100;
private readonly GeneratorHelper<TResult> _helper;

/// <summary>
/// Initializes a new instance of the <see cref="OutcomeGenerator{TResult}"/> class.
/// </summary>
public OutcomeGenerator()
: this(RandomUtil.Instance.Next)
{
}

internal OutcomeGenerator(Func<int, int> weightGenerator)
=> _helper = new GeneratorHelper<TResult>(weightGenerator);

/// <summary>
/// Registers an exception generator delegate.
/// </summary>
/// <param name="generator">The delegate that generates the exception.</param>
/// <param name="weight">The weight assigned to this generator. Defaults to <c>100</c>.</param>
/// <returns>The current instance of <see cref="OutcomeGenerator{TResult}"/>.</returns>
public OutcomeGenerator<TResult> AddException(Func<Exception> generator, int weight = DefaultWeight)
{
Guard.NotNull(generator);

_helper.AddOutcome(_ => Outcome.FromException<TResult>(generator()), weight);

return this;
}

/// <summary>
/// Registers an exception generator delegate that accepts a <see cref="ResilienceContext"/>.
/// </summary>
/// <param name="generator">The delegate that generates the exception, accepting a <see cref="ResilienceContext"/>.</param>
/// <param name="weight">The weight assigned to this generator. Defaults to <c>100</c>.</param>
/// <returns>The current instance of <see cref="OutcomeGenerator{TResult}"/>.</returns>
public OutcomeGenerator<TResult> AddException(Func<ResilienceContext, Exception> generator, int weight = DefaultWeight)
{
Guard.NotNull(generator);

_helper.AddOutcome(context => Outcome.FromException<TResult>(generator(context)), weight);

return this;
}

/// <summary>
/// Registers an exception generator for a specific exception type, using the default constructor of that exception.
/// </summary>
/// <typeparam name="TException">The type of the exception to generate.</typeparam>
/// <param name="weight">The weight assigned to this generator. Defaults to <c>100</c>.</param>
/// <returns>The current instance of <see cref="OutcomeGenerator{TResult}"/>.</returns>
public OutcomeGenerator<TResult> AddException<TException>(int weight = DefaultWeight)
where TException : Exception, new()
{
_helper.AddOutcome(_ => Outcome.FromException<TResult>(new TException()), weight);

return this;
}

/// <summary>
/// Registers a result generator.
/// </summary>
/// <param name="generator">The delegate that generates the result.</param>
/// <param name="weight">The weight assigned to this generator. Defaults to <c>100</c>.</param>
/// <returns>The current instance of <see cref="OutcomeGenerator{TResult}"/>.</returns>
public OutcomeGenerator<TResult> AddResult(Func<TResult> generator, int weight = DefaultWeight)
{
Guard.NotNull(generator);

_helper.AddOutcome(_ => Outcome.FromResult(generator()), weight);

return this;
}

/// <summary>
/// Registers a result generator.
/// </summary>
/// <param name="generator">The delegate that generates the result, accepting a <see cref="ResilienceContext"/>.</param>
/// <param name="weight">The weight assigned to this generator. Defaults to <c>100</c>.</param>
/// <returns>The current instance of <see cref="OutcomeGenerator{TResult}"/>.</returns>
public OutcomeGenerator<TResult> AddResult(Func<ResilienceContext, TResult> generator, int weight = DefaultWeight)
{
Guard.NotNull(generator);

_helper.AddOutcome(context => Outcome.FromResult(generator(context)), weight);

return this;
}

/// <summary>
/// Implicit conversion to <see cref="OutcomeStrategyOptions{TResult}.OutcomeGenerator"/>.
/// </summary>
/// <param name="generator">The generator instance.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator Func<OutcomeGeneratorArguments, ValueTask<Outcome<TResult>?>>(OutcomeGenerator<TResult> generator)
{
Guard.NotNull(generator);

var generatorDelegate = generator._helper.CreateGenerator();

return args => new ValueTask<Outcome<TResult>?>(generatorDelegate(args.Context));
}
}

24 changes: 0 additions & 24 deletions src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs

This file was deleted.

24 changes: 21 additions & 3 deletions src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
namespace Polly.Simmy.Outcomes;
using System.ComponentModel.DataAnnotations;

/// <inheritdoc/>
public class OutcomeStrategyOptions : OutcomeStrategyOptions<object>
namespace Polly.Simmy.Outcomes;

/// <summary>
/// Represents the options for the Outcome chaos strategy.
/// </summary>
/// <typeparam name="TResult">The type of the outcome that was injected.</typeparam>
public class OutcomeStrategyOptions<TResult> : MonkeyStrategyOptions
{
/// <summary>
/// Gets or sets the delegate that's invoked when the outcome is injected.
/// </summary>
/// <remarks>
/// Defaults to <see langword="null"/>.
/// </remarks>
public Func<OnOutcomeInjectedArguments<TResult>, ValueTask>? OnOutcomeInjected { get; set; }

/// <summary>
/// Gets or sets the outcome generator to be injected for a given execution.
/// </summary>
[Required]
public Func<OutcomeGeneratorArguments, ValueTask<Outcome<TResult>?>> OutcomeGenerator { get; set; } = default!;
}
52 changes: 52 additions & 0 deletions src/Polly.Core/Simmy/Utils/GeneratorHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace Polly.Simmy.Utils;

internal sealed class GeneratorHelper<TResult>
{
private readonly Func<int, int> _weightGenerator;

private readonly List<int> _weights = [];
private readonly List<Func<ResilienceContext, Outcome<TResult>>> _factories = [];
private int _totalWeight;

public GeneratorHelper(Func<int, int> weightGenerator) => _weightGenerator = weightGenerator;

public void AddOutcome(Func<ResilienceContext, Outcome<TResult>> generator, int weight)
{
Guard.NotNull(generator);

_totalWeight += weight;
_factories.Add(generator);
_weights.Add(weight);
}

internal Func<ResilienceContext, Outcome<TResult>?> CreateGenerator()
{
if (_factories.Count == 0)
{
return _ => null;
}

var totalWeight = _totalWeight;
var factories = _factories.ToArray();
var weights = _weights.ToArray();
var generator = _weightGenerator;

return context =>
{
var generatedWeight = generator(totalWeight);
var weight = 0;
for (var i = 0; i < factories.Length; i++)
{
weight += weights[i];
if (generatedWeight < weight)
{
return factories[i](context);
}
}
return null;
};
}
}

2 changes: 2 additions & 0 deletions src/Polly.Core/Utils/RandomUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ internal sealed class RandomUtil
public RandomUtil(int? seed) => _random = new ThreadLocal<Random>(() => seed == null ? new Random() : new Random(seed.Value));

public double NextDouble() => _random.Value!.NextDouble();

public int Next(int maxValue) => _random.Value!.Next(maxValue);
}
Loading

0 comments on commit cf00835

Please sign in to comment.