diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index 97bb9afd8e9..cd6bf54fba4 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -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! generator, int weight = 100) -> Polly.Simmy.Fault.FaultGenerator! +Polly.Simmy.Fault.FaultGenerator.AddException(System.Func! generator, int weight = 100) -> Polly.Simmy.Fault.FaultGenerator! +Polly.Simmy.Fault.FaultGenerator.AddException(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 @@ -84,12 +89,17 @@ Polly.Simmy.Outcomes.OnOutcomeInjectedArguments.Context.get -> Polly.Re Polly.Simmy.Outcomes.OnOutcomeInjectedArguments.OnOutcomeInjectedArguments() -> void Polly.Simmy.Outcomes.OnOutcomeInjectedArguments.OnOutcomeInjectedArguments(Polly.ResilienceContext! context, Polly.Outcome outcome) -> void Polly.Simmy.Outcomes.OnOutcomeInjectedArguments.Outcome.get -> Polly.Outcome +Polly.Simmy.Outcomes.OutcomeGenerator +Polly.Simmy.Outcomes.OutcomeGenerator.AddException(System.Func! generator, int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator! +Polly.Simmy.Outcomes.OutcomeGenerator.AddException(System.Func! generator, int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator! +Polly.Simmy.Outcomes.OutcomeGenerator.AddException(int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator! +Polly.Simmy.Outcomes.OutcomeGenerator.AddResult(System.Func! generator, int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator! +Polly.Simmy.Outcomes.OutcomeGenerator.AddResult(System.Func! generator, int weight = 100) -> Polly.Simmy.Outcomes.OutcomeGenerator! +Polly.Simmy.Outcomes.OutcomeGenerator.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 Polly.Simmy.Outcomes.OutcomeStrategyOptions.OnOutcomeInjected.get -> System.Func, System.Threading.Tasks.ValueTask>? Polly.Simmy.Outcomes.OutcomeStrategyOptions.OnOutcomeInjected.set -> void @@ -98,9 +108,11 @@ Polly.Simmy.Outcomes.OutcomeStrategyOptions.OutcomeGenerator.set -> voi Polly.Simmy.Outcomes.OutcomeStrategyOptions.OutcomeStrategyOptions() -> void static Polly.Simmy.BehaviorPipelineBuilderExtensions.AddChaosBehavior(this TBuilder! builder, double injectionRate, System.Func! behavior) -> TBuilder! static Polly.Simmy.BehaviorPipelineBuilderExtensions.AddChaosBehavior(this TBuilder! builder, Polly.Simmy.Behavior.BehaviorStrategyOptions! options) -> TBuilder! +static Polly.Simmy.Fault.FaultGenerator.implicit operator System.Func>!(Polly.Simmy.Fault.FaultGenerator! generator) -> System.Func>! static Polly.Simmy.FaultPipelineBuilderExtensions.AddChaosFault(this TBuilder! builder, double injectionRate, System.Func! faultGenerator) -> TBuilder! static Polly.Simmy.FaultPipelineBuilderExtensions.AddChaosFault(this TBuilder! builder, Polly.Simmy.Fault.FaultStrategyOptions! options) -> TBuilder! static Polly.Simmy.LatencyPipelineBuilderExtensions.AddChaosLatency(this TBuilder! builder, double injectionRate, System.TimeSpan latency) -> TBuilder! static Polly.Simmy.LatencyPipelineBuilderExtensions.AddChaosLatency(this TBuilder! builder, Polly.Simmy.Latency.LatencyStrategyOptions! options) -> TBuilder! static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosResult(this Polly.ResiliencePipelineBuilder! builder, double injectionRate, System.Func! resultGenerator) -> Polly.ResiliencePipelineBuilder! static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosResult(this Polly.ResiliencePipelineBuilder! builder, Polly.Simmy.Outcomes.OutcomeStrategyOptions! options) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.Outcomes.OutcomeGenerator.implicit operator System.Func?>>!(Polly.Simmy.Outcomes.OutcomeGenerator! generator) -> System.Func?>>! diff --git a/src/Polly.Core/Simmy/Fault/FaultGenerator.cs b/src/Polly.Core/Simmy/Fault/FaultGenerator.cs new file mode 100644 index 00000000000..374faee6a70 --- /dev/null +++ b/src/Polly.Core/Simmy/Fault/FaultGenerator.cs @@ -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 + +/// +/// A generator for creating faults (exceptions) using registered delegate functions. +/// +/// +/// An instance of this class can be assigned to the property. +/// +public sealed class FaultGenerator +{ + private const int DefaultWeight = 100; + + private readonly GeneratorHelper _helper; + + /// + /// Initializes a new instance of the class. + /// + public FaultGenerator() + => _helper = new GeneratorHelper(RandomUtil.Instance.Next); + + /// + /// Registers an exception generator delegate. + /// + /// The delegate that generates the exception. + /// The weight assigned to this generator. Defaults to 100. + /// The current instance of . + public FaultGenerator AddException(Func generator, int weight = DefaultWeight) + { + Guard.NotNull(generator); + + _helper.AddOutcome(_ => Outcome.FromException(generator()), weight); + + return this; + } + + /// + /// Registers an exception generator delegate that accepts a . + /// + /// The delegate that generates the exception, accepting a . + /// The weight assigned to this generator. Defaults to 100. + /// The current instance of . + public FaultGenerator AddException(Func generator, int weight = DefaultWeight) + { + Guard.NotNull(generator); + + _helper.AddOutcome(context => Outcome.FromException(generator(context)), weight); + + return this; + } + + /// + /// Registers an exception generator for a specific exception type, using the default constructor of that exception. + /// + /// The type of the exception to generate. + /// The weight assigned to this generator. Defaults to 100. + /// The current instance of . + public FaultGenerator AddException(int weight = DefaultWeight) + where TException : Exception, new() + { + _helper.AddOutcome(_ => Outcome.FromException(new TException()), weight); + + return this; + } + + /// + /// Provides an implicit conversion from to a delegate compatible with . + /// + /// The instance of . + [EditorBrowsable(EditorBrowsableState.Never)] + public static implicit operator Func>(FaultGenerator generator) + { + Guard.NotNull(generator); + + var generatorDelegate = generator._helper.CreateGenerator(); + + return args => new ValueTask(generatorDelegate(args.Context)!.Value.Exception); + } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeGenerator.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeGenerator.cs new file mode 100644 index 00000000000..3d1439ab80f --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeGenerator.cs @@ -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 + +/// +/// Generator that produces faults such as exceptions or results. +/// +/// The type of the result. +/// +/// An instance of this class is assignable to . +/// +public sealed class OutcomeGenerator +{ + private const int DefaultWeight = 100; + private readonly GeneratorHelper _helper; + + /// + /// Initializes a new instance of the class. + /// + public OutcomeGenerator() + : this(RandomUtil.Instance.Next) + { + } + + internal OutcomeGenerator(Func weightGenerator) + => _helper = new GeneratorHelper(weightGenerator); + + /// + /// Registers an exception generator delegate. + /// + /// The delegate that generates the exception. + /// The weight assigned to this generator. Defaults to 100. + /// The current instance of . + public OutcomeGenerator AddException(Func generator, int weight = DefaultWeight) + { + Guard.NotNull(generator); + + _helper.AddOutcome(_ => Outcome.FromException(generator()), weight); + + return this; + } + + /// + /// Registers an exception generator delegate that accepts a . + /// + /// The delegate that generates the exception, accepting a . + /// The weight assigned to this generator. Defaults to 100. + /// The current instance of . + public OutcomeGenerator AddException(Func generator, int weight = DefaultWeight) + { + Guard.NotNull(generator); + + _helper.AddOutcome(context => Outcome.FromException(generator(context)), weight); + + return this; + } + + /// + /// Registers an exception generator for a specific exception type, using the default constructor of that exception. + /// + /// The type of the exception to generate. + /// The weight assigned to this generator. Defaults to 100. + /// The current instance of . + public OutcomeGenerator AddException(int weight = DefaultWeight) + where TException : Exception, new() + { + _helper.AddOutcome(_ => Outcome.FromException(new TException()), weight); + + return this; + } + + /// + /// Registers a result generator. + /// + /// The delegate that generates the result. + /// The weight assigned to this generator. Defaults to 100. + /// The current instance of . + public OutcomeGenerator AddResult(Func generator, int weight = DefaultWeight) + { + Guard.NotNull(generator); + + _helper.AddOutcome(_ => Outcome.FromResult(generator()), weight); + + return this; + } + + /// + /// Registers a result generator. + /// + /// The delegate that generates the result, accepting a . + /// The weight assigned to this generator. Defaults to 100. + /// The current instance of . + public OutcomeGenerator AddResult(Func generator, int weight = DefaultWeight) + { + Guard.NotNull(generator); + + _helper.AddOutcome(context => Outcome.FromResult(generator(context)), weight); + + return this; + } + + /// + /// Implicit conversion to . + /// + /// The generator instance. + [EditorBrowsable(EditorBrowsableState.Never)] + public static implicit operator Func?>>(OutcomeGenerator generator) + { + Guard.NotNull(generator); + + var generatorDelegate = generator._helper.CreateGenerator(); + + return args => new ValueTask?>(generatorDelegate(args.Context)); + } +} + diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs deleted file mode 100644 index 4236d870254..00000000000 --- a/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Polly.Simmy.Outcomes; - -/// -/// Represents the options for the Outcome chaos strategy. -/// -/// The type of the outcome that was injected. -public class OutcomeStrategyOptions : MonkeyStrategyOptions -{ - /// - /// Gets or sets the delegate that's raised when the outcome is injected. - /// - /// - /// Defaults to . - /// - public Func, ValueTask>? OnOutcomeInjected { get; set; } - - /// - /// Gets or sets the outcome generator to be injected for a given execution. - /// - [Required] - public Func?>> OutcomeGenerator { get; set; } = default!; -} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.cs index 7a29aa8ef5d..fa8f21fddad 100644 --- a/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.cs +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.cs @@ -1,6 +1,24 @@ -namespace Polly.Simmy.Outcomes; +using System.ComponentModel.DataAnnotations; -/// -public class OutcomeStrategyOptions : OutcomeStrategyOptions +namespace Polly.Simmy.Outcomes; + +/// +/// Represents the options for the Outcome chaos strategy. +/// +/// The type of the outcome that was injected. +public class OutcomeStrategyOptions : MonkeyStrategyOptions { + /// + /// Gets or sets the delegate that's invoked when the outcome is injected. + /// + /// + /// Defaults to . + /// + public Func, ValueTask>? OnOutcomeInjected { get; set; } + + /// + /// Gets or sets the outcome generator to be injected for a given execution. + /// + [Required] + public Func?>> OutcomeGenerator { get; set; } = default!; } diff --git a/src/Polly.Core/Simmy/Utils/GeneratorHelper.cs b/src/Polly.Core/Simmy/Utils/GeneratorHelper.cs new file mode 100644 index 00000000000..6ec4090a0f2 --- /dev/null +++ b/src/Polly.Core/Simmy/Utils/GeneratorHelper.cs @@ -0,0 +1,52 @@ +namespace Polly.Simmy.Utils; + +internal sealed class GeneratorHelper +{ + private readonly Func _weightGenerator; + + private readonly List _weights = []; + private readonly List>> _factories = []; + private int _totalWeight; + + public GeneratorHelper(Func weightGenerator) => _weightGenerator = weightGenerator; + + public void AddOutcome(Func> generator, int weight) + { + Guard.NotNull(generator); + + _totalWeight += weight; + _factories.Add(generator); + _weights.Add(weight); + } + + internal Func?> 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; + }; + } +} + diff --git a/src/Polly.Core/Utils/RandomUtil.cs b/src/Polly.Core/Utils/RandomUtil.cs index aac063fcc7a..de7bc6ff6ce 100644 --- a/src/Polly.Core/Utils/RandomUtil.cs +++ b/src/Polly.Core/Utils/RandomUtil.cs @@ -13,4 +13,6 @@ internal sealed class RandomUtil public RandomUtil(int? seed) => _random = new ThreadLocal(() => seed == null ? new Random() : new Random(seed.Value)); public double NextDouble() => _random.Value!.NextDouble(); + + public int Next(int maxValue) => _random.Value!.Next(maxValue); } diff --git a/test/Polly.Core.Tests/Simmy/Fault/FaultGeneratorTests.cs b/test/Polly.Core.Tests/Simmy/Fault/FaultGeneratorTests.cs new file mode 100644 index 00000000000..0ffe3c121cf --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Fault/FaultGeneratorTests.cs @@ -0,0 +1,49 @@ +using System; +using Polly.Simmy.Fault; + +namespace Polly.Core.Tests.Simmy.Fault; + +public class FaultGeneratorTests +{ + [Fact] + public void AddException_Generic_Ok() + { + var generator = new FaultGenerator(); + + generator.AddException(); + + Generate(generator).Should().BeOfType(); + } + + [Fact] + public void AddException_Factory_Ok() + { + var generator = new FaultGenerator(); + + generator.AddException(() => new InvalidOperationException()); + + Generate(generator).Should().BeOfType(); + } + + [Fact] + public void AddException_FactoryWithResilienceContext_Ok() + { + var generator = new FaultGenerator(); + + generator.AddException(context => + { + context.Should().NotBeNull(); + + return new InvalidOperationException(); + }); + + Generate(generator).Should().BeOfType(); + } + + private static Exception? Generate(FaultGenerator generator) + { + Func> func = generator; + + return func(new FaultGeneratorArguments(ResilienceContextPool.Shared.Get())).AsTask().Result; + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeGeneratorTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeGeneratorTests.cs new file mode 100644 index 00000000000..924de797690 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeGeneratorTests.cs @@ -0,0 +1,77 @@ +using System; +using Polly.Simmy.Outcomes; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class OutcomeGeneratorTests +{ + [Fact] + public void AddException_Generic_Ok() + { + var generator = new OutcomeGenerator(); + + generator.AddException(); + + Generate(generator)!.Value.Exception.Should().BeOfType(); + } + + [Fact] + public void AddException_Factory_Ok() + { + var generator = new OutcomeGenerator(); + + generator.AddException(() => new InvalidOperationException()); + + Generate(generator)!.Value.Exception.Should().BeOfType(); + } + + [Fact] + public void AddException_FactoryWithResilienceContext_Ok() + { + var generator = new OutcomeGenerator(); + + generator.AddException(context => + { + context.Should().NotBeNull(); + + return new InvalidOperationException(); + }); + + Generate(generator)!.Value.Exception.Should().BeOfType(); + } + + [Fact] + public void AddResult_Factory_Ok() + { + var generator = new OutcomeGenerator(); + + generator.AddResult(() => + { + return "dummy"; + }); + + Generate(generator)!.Value.Result.Should().Be("dummy"); + } + + [Fact] + public void AddResult_FactoryWithResilienceContext_Ok() + { + var generator = new OutcomeGenerator(); + + generator.AddResult(context => + { + context.Should().NotBeNull(); + + return "dummy"; + }); + + Generate(generator)!.Value.Result.Should().Be("dummy"); + } + + private static Outcome? Generate(OutcomeGenerator generator) + { + Func?>> func = generator; + + return func(new OutcomeGeneratorArguments(ResilienceContextPool.Shared.Get())).AsTask().Result; + } +} diff --git a/test/Polly.Core.Tests/Simmy/Utils/GeneratorHelperTests.cs b/test/Polly.Core.Tests/Simmy/Utils/GeneratorHelperTests.cs new file mode 100644 index 00000000000..258124cec77 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Utils/GeneratorHelperTests.cs @@ -0,0 +1,56 @@ +using System; +using Polly.Simmy.Utils; + +namespace Polly.Core.Tests.Simmy.Utils; + +public class GeneratorHelperTests +{ + [Fact] + public void CreateGenerator_NoGenerators_Ok() + { + var helper = new GeneratorHelper(_ => 10); + + helper.CreateGenerator()(ResilienceContextPool.Shared.Get()).Should().BeNull(); + } + + [Fact] + public void AddOutcome_EnsureWeightRespected() + { + int weight = 0; + int maxWeight = 0; + var context = ResilienceContextPool.Shared.Get(); + + var helper = new GeneratorHelper(max => + { + maxWeight = max; + return weight; + }); + + helper.AddOutcome(_ => Outcome.FromResult(1), 40); + helper.AddOutcome(_ => Outcome.FromResult(2), 80); + + var generator = helper.CreateGenerator(); + + weight = 0; + generator(context)!.Value.Result.Should().Be(1); + weight = 39; + generator(context)!.Value.Result.Should().Be(1); + + weight = 40; + generator(context)!.Value.Result.Should().Be(2); + + maxWeight.Should().Be(120); + } + + [Fact] + public void Generator_OutsideRange_ReturnsNull() + { + var context = ResilienceContextPool.Shared.Get(); + var helper = new GeneratorHelper(_ => 1000); + + helper.AddOutcome(_ => Outcome.FromResult(1), 40); + + var generator = helper.CreateGenerator(); + generator(context).Should().BeNull(); + } +}