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 FaultGenerator and OutcomeGenerator<T> #1911

Merged
merged 3 commits into from
Jan 21, 2024
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
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