diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index 74c19f8c4ed..1ce0be49a51 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -321,6 +321,104 @@ Polly.Retry.RetryStrategyOptions.ShouldHandle.set -> void Polly.Retry.RetryStrategyOptions.UseJitter.get -> bool Polly.Retry.RetryStrategyOptions.UseJitter.set -> void Polly.RetryResiliencePipelineBuilderExtensions +Polly.Simmy.Behavior.BehaviorActionArguments +Polly.Simmy.Behavior.BehaviorActionArguments.BehaviorActionArguments() -> void +Polly.Simmy.Behavior.BehaviorActionArguments.BehaviorActionArguments(Polly.ResilienceContext! context) -> void +Polly.Simmy.Behavior.BehaviorActionArguments.Context.get -> Polly.ResilienceContext! +Polly.Simmy.Behavior.BehaviorStrategyOptions +Polly.Simmy.Behavior.BehaviorStrategyOptions.BehaviorAction.get -> System.Func? +Polly.Simmy.Behavior.BehaviorStrategyOptions.BehaviorAction.set -> void +Polly.Simmy.Behavior.BehaviorStrategyOptions.BehaviorStrategyOptions() -> void +Polly.Simmy.Behavior.BehaviorStrategyOptions.OnBehaviorInjected.get -> System.Func? +Polly.Simmy.Behavior.BehaviorStrategyOptions.OnBehaviorInjected.set -> void +Polly.Simmy.Behavior.OnBehaviorInjectedArguments +Polly.Simmy.Behavior.OnBehaviorInjectedArguments.Context.get -> Polly.ResilienceContext! +Polly.Simmy.Behavior.OnBehaviorInjectedArguments.OnBehaviorInjectedArguments() -> void +Polly.Simmy.Behavior.OnBehaviorInjectedArguments.OnBehaviorInjectedArguments(Polly.ResilienceContext! context) -> void +Polly.Simmy.BehaviorPipelineBuilderExtensions +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.InjectionRateGeneratorArguments +Polly.Simmy.InjectionRateGeneratorArguments.Context.get -> Polly.ResilienceContext! +Polly.Simmy.InjectionRateGeneratorArguments.InjectionRateGeneratorArguments() -> void +Polly.Simmy.InjectionRateGeneratorArguments.InjectionRateGeneratorArguments(Polly.ResilienceContext! context) -> void +Polly.Simmy.Latency.LatencyGeneratorArguments +Polly.Simmy.Latency.LatencyGeneratorArguments.Context.get -> Polly.ResilienceContext! +Polly.Simmy.Latency.LatencyGeneratorArguments.LatencyGeneratorArguments() -> void +Polly.Simmy.Latency.LatencyGeneratorArguments.LatencyGeneratorArguments(Polly.ResilienceContext! context) -> void +Polly.Simmy.Latency.LatencyStrategyOptions +Polly.Simmy.Latency.LatencyStrategyOptions.Latency.get -> System.TimeSpan +Polly.Simmy.Latency.LatencyStrategyOptions.Latency.set -> void +Polly.Simmy.Latency.LatencyStrategyOptions.LatencyGenerator.get -> System.Func>? +Polly.Simmy.Latency.LatencyStrategyOptions.LatencyGenerator.set -> void +Polly.Simmy.Latency.LatencyStrategyOptions.LatencyStrategyOptions() -> void +Polly.Simmy.Latency.LatencyStrategyOptions.OnLatency.get -> System.Func? +Polly.Simmy.Latency.LatencyStrategyOptions.OnLatency.set -> void +Polly.Simmy.Latency.OnLatencyArguments +Polly.Simmy.Latency.OnLatencyArguments.Context.get -> Polly.ResilienceContext! +Polly.Simmy.Latency.OnLatencyArguments.Latency.get -> System.TimeSpan +Polly.Simmy.Latency.OnLatencyArguments.OnLatencyArguments() -> void +Polly.Simmy.Latency.OnLatencyArguments.OnLatencyArguments(Polly.ResilienceContext! context, System.TimeSpan latency) -> void +Polly.Simmy.LatencyPipelineBuilderExtensions +Polly.Simmy.MonkeyStrategy +Polly.Simmy.MonkeyStrategy.MonkeyStrategy(Polly.Simmy.MonkeyStrategyOptions! options) -> void +Polly.Simmy.MonkeyStrategy.ShouldInjectAsync(Polly.ResilienceContext! context) -> System.Threading.Tasks.ValueTask +Polly.Simmy.MonkeyStrategy +Polly.Simmy.MonkeyStrategy.MonkeyStrategy(Polly.Simmy.MonkeyStrategyOptions! options) -> void +Polly.Simmy.MonkeyStrategy.ShouldInjectAsync(Polly.ResilienceContext! context) -> System.Threading.Tasks.ValueTask +Polly.Simmy.MonkeyStrategyOptions +Polly.Simmy.MonkeyStrategyOptions.MonkeyStrategyOptions() -> void +Polly.Simmy.MonkeyStrategyOptions +Polly.Simmy.MonkeyStrategyOptions.Enabled.get -> bool +Polly.Simmy.MonkeyStrategyOptions.Enabled.set -> void +Polly.Simmy.MonkeyStrategyOptions.EnabledGenerator.get -> System.Func>? +Polly.Simmy.MonkeyStrategyOptions.EnabledGenerator.set -> void +Polly.Simmy.MonkeyStrategyOptions.InjectionRate.get -> double +Polly.Simmy.MonkeyStrategyOptions.InjectionRate.set -> void +Polly.Simmy.MonkeyStrategyOptions.InjectionRateGenerator.get -> System.Func>? +Polly.Simmy.MonkeyStrategyOptions.InjectionRateGenerator.set -> void +Polly.Simmy.MonkeyStrategyOptions.MonkeyStrategyOptions() -> void +Polly.Simmy.MonkeyStrategyOptions.Randomizer.get -> System.Func! +Polly.Simmy.MonkeyStrategyOptions.Randomizer.set -> void +Polly.Simmy.OutcomePipelineBuilderExtensions +Polly.Simmy.Outcomes.FaultGeneratorArguments +Polly.Simmy.Outcomes.FaultGeneratorArguments.Context.get -> Polly.ResilienceContext! +Polly.Simmy.Outcomes.FaultGeneratorArguments.FaultGeneratorArguments() -> void +Polly.Simmy.Outcomes.FaultGeneratorArguments.FaultGeneratorArguments(Polly.ResilienceContext! context) -> void +Polly.Simmy.Outcomes.FaultStrategyOptions +Polly.Simmy.Outcomes.FaultStrategyOptions.Fault.get -> System.Exception? +Polly.Simmy.Outcomes.FaultStrategyOptions.Fault.set -> void +Polly.Simmy.Outcomes.FaultStrategyOptions.FaultGenerator.get -> System.Func>? +Polly.Simmy.Outcomes.FaultStrategyOptions.FaultGenerator.set -> void +Polly.Simmy.Outcomes.FaultStrategyOptions.FaultStrategyOptions() -> void +Polly.Simmy.Outcomes.FaultStrategyOptions.OnFaultInjected.get -> System.Func? +Polly.Simmy.Outcomes.FaultStrategyOptions.OnFaultInjected.set -> void +Polly.Simmy.Outcomes.OnFaultInjectedArguments +Polly.Simmy.Outcomes.OnFaultInjectedArguments.Context.get -> Polly.ResilienceContext! +Polly.Simmy.Outcomes.OnFaultInjectedArguments.Fault.get -> System.Exception! +Polly.Simmy.Outcomes.OnFaultInjectedArguments.OnFaultInjectedArguments() -> void +Polly.Simmy.Outcomes.OnFaultInjectedArguments.OnFaultInjectedArguments(Polly.ResilienceContext! context, System.Exception! fault) -> void +Polly.Simmy.Outcomes.OnOutcomeInjectedArguments +Polly.Simmy.Outcomes.OnOutcomeInjectedArguments.Context.get -> Polly.ResilienceContext! +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.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 +Polly.Simmy.Outcomes.OutcomeStrategyOptions.Outcome.get -> Polly.Outcome? +Polly.Simmy.Outcomes.OutcomeStrategyOptions.Outcome.set -> void +Polly.Simmy.Outcomes.OutcomeStrategyOptions.OutcomeGenerator.get -> System.Func?>>? +Polly.Simmy.Outcomes.OutcomeStrategyOptions.OutcomeGenerator.set -> void +Polly.Simmy.Outcomes.OutcomeStrategyOptions.OutcomeStrategyOptions() -> void Polly.StrategyBuilderContext Polly.StrategyBuilderContext.Telemetry.get -> Polly.Telemetry.ResilienceStrategyTelemetry! Polly.Telemetry.ExecutionAttemptArguments @@ -414,6 +512,19 @@ static Polly.ResiliencePipelineBuilderExtensions.AddStrategy(this TBui static Polly.ResiliencePipelineBuilderExtensions.AddStrategy(this Polly.ResiliencePipelineBuilder! builder, System.Func!>! factory, Polly.ResilienceStrategyOptions! options) -> Polly.ResiliencePipelineBuilder! static Polly.RetryResiliencePipelineBuilderExtensions.AddRetry(this Polly.ResiliencePipelineBuilder! builder, Polly.Retry.RetryStrategyOptions! options) -> Polly.ResiliencePipelineBuilder! static Polly.RetryResiliencePipelineBuilderExtensions.AddRetry(this Polly.ResiliencePipelineBuilder! builder, Polly.Retry.RetryStrategyOptions! options) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.BehaviorPipelineBuilderExtensions.AddChaosBehavior(this TBuilder! builder, bool enabled, double injectionRate, System.Func! behavior) -> TBuilder! +static Polly.Simmy.BehaviorPipelineBuilderExtensions.AddChaosBehavior(this TBuilder! builder, Polly.Simmy.Behavior.BehaviorStrategyOptions! options) -> TBuilder! +static Polly.Simmy.LatencyPipelineBuilderExtensions.AddChaosLatency(this TBuilder! builder, bool enabled, double injectionRate, System.TimeSpan latency) -> TBuilder! +static Polly.Simmy.LatencyPipelineBuilderExtensions.AddChaosLatency(this TBuilder! builder, Polly.Simmy.Latency.LatencyStrategyOptions! options) -> TBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosFault(this Polly.ResiliencePipelineBuilder! builder, bool enabled, double injectionRate, System.Exception! fault) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosFault(this Polly.ResiliencePipelineBuilder! builder, bool enabled, double injectionRate, System.Func! faultGenerator) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosFault(this Polly.ResiliencePipelineBuilder! builder, Polly.Simmy.Outcomes.FaultStrategyOptions! options) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosFault(this Polly.ResiliencePipelineBuilder! builder, bool enabled, double injectionRate, System.Exception! fault) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosFault(this Polly.ResiliencePipelineBuilder! builder, bool enabled, double injectionRate, System.Func! faultGenerator) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosFault(this Polly.ResiliencePipelineBuilder! builder, Polly.Simmy.Outcomes.FaultStrategyOptions! options) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosResult(this Polly.ResiliencePipelineBuilder! builder, bool enabled, double injectionRate, System.Func! outcomeGenerator) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosResult(this Polly.ResiliencePipelineBuilder! builder, bool enabled, double injectionRate, TResult result) -> Polly.ResiliencePipelineBuilder! +static Polly.Simmy.OutcomePipelineBuilderExtensions.AddChaosResult(this Polly.ResiliencePipelineBuilder! builder, Polly.Simmy.Outcomes.OutcomeStrategyOptions! options) -> Polly.ResiliencePipelineBuilder! static Polly.TimeoutResiliencePipelineBuilderExtensions.AddTimeout(this TBuilder! builder, Polly.Timeout.TimeoutStrategyOptions! options) -> TBuilder! static Polly.TimeoutResiliencePipelineBuilderExtensions.AddTimeout(this TBuilder! builder, System.TimeSpan timeout) -> TBuilder! static readonly Polly.ResiliencePipeline.Empty -> Polly.ResiliencePipeline! diff --git a/src/Polly.Core/Simmy/Behavior/BehaviorActionArguments.cs b/src/Polly.Core/Simmy/Behavior/BehaviorActionArguments.cs new file mode 100644 index 00000000000..2df7cdd1724 --- /dev/null +++ b/src/Polly.Core/Simmy/Behavior/BehaviorActionArguments.cs @@ -0,0 +1,20 @@ +namespace Polly.Simmy.Behavior; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by the behavior chaos strategy to execute a user's delegate custom action. +/// +public readonly struct BehaviorActionArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The context associated with the execution of a user-provided callback. + public BehaviorActionArguments(ResilienceContext context) => Context = context; + + /// + /// Gets the ResilienceContext instance. + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Simmy/Behavior/BehaviorChaosStrategy.cs b/src/Polly.Core/Simmy/Behavior/BehaviorChaosStrategy.cs new file mode 100644 index 00000000000..654fb93f45f --- /dev/null +++ b/src/Polly.Core/Simmy/Behavior/BehaviorChaosStrategy.cs @@ -0,0 +1,50 @@ +using Polly.Telemetry; + +namespace Polly.Simmy.Behavior; + +internal sealed class BehaviorChaosStrategy : MonkeyStrategy +{ + private readonly ResilienceStrategyTelemetry _telemetry; + + public BehaviorChaosStrategy( + BehaviorStrategyOptions options, + ResilienceStrategyTelemetry telemetry) + : base(options) + { + _telemetry = telemetry; + OnBehaviorInjected = options.OnBehaviorInjected; + Behavior = options.BehaviorAction!; + } + + public Func? OnBehaviorInjected { get; } + + public Func Behavior { get; } + + protected internal override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + try + { + if (await ShouldInjectAsync(context).ConfigureAwait(context.ContinueOnCapturedContext)) + { + await Behavior(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); + + var args = new OnBehaviorInjectedArguments(context); + _telemetry.Report(new(ResilienceEventSeverity.Information, BehaviorConstants.OnBehaviorInjectedEvent), context, args); + + if (OnBehaviorInjected is not null) + { + await OnBehaviorInjected(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + } + + return await StrategyHelper.ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + catch (OperationCanceledException e) + { + return new Outcome(e); + } + } +} diff --git a/src/Polly.Core/Simmy/Behavior/BehaviorConstants.cs b/src/Polly.Core/Simmy/Behavior/BehaviorConstants.cs new file mode 100644 index 00000000000..91341bdb948 --- /dev/null +++ b/src/Polly.Core/Simmy/Behavior/BehaviorConstants.cs @@ -0,0 +1,6 @@ +namespace Polly.Simmy.Behavior; + +internal static class BehaviorConstants +{ + public const string OnBehaviorInjectedEvent = "OnBehaviorInjected"; +} diff --git a/src/Polly.Core/Simmy/Behavior/BehaviorPipelineBuilderExtensions.cs b/src/Polly.Core/Simmy/Behavior/BehaviorPipelineBuilderExtensions.cs new file mode 100644 index 00000000000..80b0173da55 --- /dev/null +++ b/src/Polly.Core/Simmy/Behavior/BehaviorPipelineBuilderExtensions.cs @@ -0,0 +1,57 @@ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Polly.Simmy.Behavior; + +namespace Polly.Simmy; + +/// +/// Extension methods for adding custom behaviors to a . +/// +public static class BehaviorPipelineBuilderExtensions +{ + /// + /// Adds a behavior chaos strategy to the builder. + /// + /// The builder type. + /// The builder instance. + /// A value that indicates whether or not the chaos strategy is enabled for a given execution. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The behavior to be injected. + /// The same builder instance. + /// Thrown when is . + /// Thrown when the options produced from the arguments are invalid. + public static TBuilder AddChaosBehavior(this TBuilder builder, bool enabled, double injectionRate, Func behavior) + where TBuilder : ResiliencePipelineBuilderBase + { + Guard.NotNull(builder); + + return builder.AddChaosBehavior(new BehaviorStrategyOptions + { + Enabled = enabled, + InjectionRate = injectionRate, + BehaviorAction = (_) => behavior() + }); + } + + /// + /// Adds a behavior chaos strategy to the builder. + /// + /// The builder type. + /// The builder instance. + /// The behavior options. + /// The same builder instance. + /// Thrown when or is . + /// Thrown when are invalid. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + public static TBuilder AddChaosBehavior(this TBuilder builder, BehaviorStrategyOptions options) + where TBuilder : ResiliencePipelineBuilderBase + { + Guard.NotNull(builder); + Guard.NotNull(options); + + return builder.AddStrategy(context => new BehaviorChaosStrategy(options, context.Telemetry), options); + } +} diff --git a/src/Polly.Core/Simmy/Behavior/BehaviorStrategyOptions.cs b/src/Polly.Core/Simmy/Behavior/BehaviorStrategyOptions.cs new file mode 100644 index 00000000000..1541fbc9b3f --- /dev/null +++ b/src/Polly.Core/Simmy/Behavior/BehaviorStrategyOptions.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace Polly.Simmy.Behavior; + +/// +/// Represents the options for the Behavior chaos strategy. +/// +public class BehaviorStrategyOptions : MonkeyStrategyOptions +{ + /// + /// Gets or sets the delegate that's raised when the custom behavior is injected. + /// + /// + /// Defaults to . + /// + public Func? OnBehaviorInjected { get; set; } + + /// + /// Gets or sets the custom behavior that is going to be injected for a given execution. + /// + /// + /// Defaults to . + /// + [Required] + public Func? BehaviorAction { get; set; } +} diff --git a/src/Polly.Core/Simmy/Behavior/OnBehaviorInjectedArguments.cs b/src/Polly.Core/Simmy/Behavior/OnBehaviorInjectedArguments.cs new file mode 100644 index 00000000000..34e68a505c7 --- /dev/null +++ b/src/Polly.Core/Simmy/Behavior/OnBehaviorInjectedArguments.cs @@ -0,0 +1,20 @@ +namespace Polly.Simmy.Behavior; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by the behavior chaos strategy to notify that a custom behavior was injected. +/// +public readonly struct OnBehaviorInjectedArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The context associated with the execution of a user-provided callback. + public OnBehaviorInjectedArguments(ResilienceContext context) => Context = context; + + /// + /// Gets the ResilienceContext instance. + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Simmy/EnabledGeneratorArguments.cs b/src/Polly.Core/Simmy/EnabledGeneratorArguments.cs new file mode 100644 index 00000000000..649e599784e --- /dev/null +++ b/src/Polly.Core/Simmy/EnabledGeneratorArguments.cs @@ -0,0 +1,20 @@ +namespace Polly.Simmy; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Defines the arguments for the . +/// +public readonly struct EnabledGeneratorArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The resilience context instance. + public EnabledGeneratorArguments(ResilienceContext context) => Context = context; + + /// + /// Gets the ResilienceContext instance. + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Simmy/InjectionRateGeneratorArguments.cs b/src/Polly.Core/Simmy/InjectionRateGeneratorArguments.cs new file mode 100644 index 00000000000..7af162378e5 --- /dev/null +++ b/src/Polly.Core/Simmy/InjectionRateGeneratorArguments.cs @@ -0,0 +1,20 @@ +namespace Polly.Simmy; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Defines the arguments for the . +/// +public readonly struct InjectionRateGeneratorArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The resilience context instance. + public InjectionRateGeneratorArguments(ResilienceContext context) => Context = context; + + /// + /// Gets the ResilienceContext instance. + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Simmy/Latency/LatencyChaosStrategy.cs b/src/Polly.Core/Simmy/Latency/LatencyChaosStrategy.cs new file mode 100644 index 00000000000..1c814acc597 --- /dev/null +++ b/src/Polly.Core/Simmy/Latency/LatencyChaosStrategy.cs @@ -0,0 +1,66 @@ +using Polly.Telemetry; + +namespace Polly.Simmy.Latency; + +#pragma warning disable S3928 // Custom ArgumentNullException message + +internal sealed class LatencyChaosStrategy : MonkeyStrategy +{ + private readonly TimeProvider _timeProvider; + private readonly ResilienceStrategyTelemetry _telemetry; + + public LatencyChaosStrategy( + LatencyStrategyOptions options, + TimeProvider timeProvider, + ResilienceStrategyTelemetry telemetry) + : base(options) + { + Latency = options.Latency; + LatencyGenerator = options.LatencyGenerator is not null ? options.LatencyGenerator : (_) => new(options.Latency); + OnLatency = options.OnLatency; + + _telemetry = telemetry; + _timeProvider = timeProvider; + } + + public Func? OnLatency { get; } + + public Func> LatencyGenerator { get; } + + public TimeSpan? Latency { get; } + + protected internal override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + try + { + if (await ShouldInjectAsync(context).ConfigureAwait(context.ContinueOnCapturedContext)) + { + var latency = await LatencyGenerator(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); + if (latency <= TimeSpan.Zero) + { + // do nothing + return await StrategyHelper.ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + + var args = new OnLatencyArguments(context, latency); + _telemetry.Report(new(ResilienceEventSeverity.Information, LatencyConstants.OnLatencyEvent), context, args); + + await _timeProvider.DelayAsync(latency, context).ConfigureAwait(context.ContinueOnCapturedContext); + + if (OnLatency is not null) + { + await OnLatency(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + } + + return await StrategyHelper.ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + catch (OperationCanceledException e) + { + return new Outcome(e); + } + } +} diff --git a/src/Polly.Core/Simmy/Latency/LatencyConstants.cs b/src/Polly.Core/Simmy/Latency/LatencyConstants.cs new file mode 100644 index 00000000000..a74b6a58a0e --- /dev/null +++ b/src/Polly.Core/Simmy/Latency/LatencyConstants.cs @@ -0,0 +1,8 @@ +namespace Polly.Simmy.Latency; + +internal static class LatencyConstants +{ + public const string OnLatencyEvent = "OnLatency"; + + public static readonly TimeSpan DefaultLatency = TimeSpan.FromSeconds(30); +} diff --git a/src/Polly.Core/Simmy/Latency/LatencyGeneratorArguments.cs b/src/Polly.Core/Simmy/Latency/LatencyGeneratorArguments.cs new file mode 100644 index 00000000000..8ef01985fee --- /dev/null +++ b/src/Polly.Core/Simmy/Latency/LatencyGeneratorArguments.cs @@ -0,0 +1,20 @@ +namespace Polly.Simmy.Latency; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by the latency chaos strategy to notify that a delayed occurred. +/// +public readonly struct LatencyGeneratorArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The context associated with the execution of a user-provided callback. + public LatencyGeneratorArguments(ResilienceContext context) => Context = context; + + /// + /// Gets the ResilienceContext instance. + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Simmy/Latency/LatencyPipelineBuilderExtensions.cs b/src/Polly.Core/Simmy/Latency/LatencyPipelineBuilderExtensions.cs new file mode 100644 index 00000000000..6c2eeb909a3 --- /dev/null +++ b/src/Polly.Core/Simmy/Latency/LatencyPipelineBuilderExtensions.cs @@ -0,0 +1,58 @@ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Polly.Simmy.Latency; + +namespace Polly.Simmy; + +/// +/// Extension methods for adding latency to a . +/// +public static class LatencyPipelineBuilderExtensions +{ + /// + /// Adds a latency chaos strategy to the builder. + /// + /// The builder type. + /// The builder instance. + /// A value that indicates whether or not the chaos strategy is enabled for a given execution. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The delay value. + /// The same builder instance. + /// Thrown when is . + /// Thrown when the options produced from the arguments are invalid. + public static TBuilder AddChaosLatency(this TBuilder builder, bool enabled, double injectionRate, TimeSpan latency) + where TBuilder : ResiliencePipelineBuilderBase + { + Guard.NotNull(builder); + + return builder.AddChaosLatency(new LatencyStrategyOptions + { + Enabled = enabled, + InjectionRate = injectionRate, + Latency = latency + }); + } + + /// + /// Adds a latency chaos strategy to the builder. + /// + /// The builder type. + /// The builder instance. + /// The latency options. + /// The same builder instance. + /// Thrown when or is . + /// Thrown when are invalid. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + public static TBuilder AddChaosLatency(this TBuilder builder, LatencyStrategyOptions options) + where TBuilder : ResiliencePipelineBuilderBase + { + Guard.NotNull(builder); + Guard.NotNull(options); + + return builder.AddStrategy(context => new LatencyChaosStrategy(options, context.TimeProvider, context.Telemetry), options); + } +} + diff --git a/src/Polly.Core/Simmy/Latency/LatencyStrategyOptions.cs b/src/Polly.Core/Simmy/Latency/LatencyStrategyOptions.cs new file mode 100644 index 00000000000..534741efa2a --- /dev/null +++ b/src/Polly.Core/Simmy/Latency/LatencyStrategyOptions.cs @@ -0,0 +1,34 @@ +namespace Polly.Simmy.Latency; + +#pragma warning disable CS8618 // Required members are not initialized in constructor since this is a DTO, default value is null + +/// +/// Represents the options for the Latency chaos strategy. +/// +public class LatencyStrategyOptions : MonkeyStrategyOptions +{ + /// + /// Gets or sets the delegate that's raised when a delay occurs. + /// + /// + /// Defaults to . + /// + public Func? OnLatency { get; set; } + + /// + /// Gets or sets the latency generator that generates the delay for a given execution. + /// + /// + /// Defaults to . Either or this property is required. + /// When this property is the is used. + /// + public Func>? LatencyGenerator { get; set; } + + /// + /// Gets or sets the delay for a given execution. + /// + /// + /// Defaults to 30 seconds. Either or this property is required. + /// + public TimeSpan Latency { get; set; } = LatencyConstants.DefaultLatency; +} diff --git a/src/Polly.Core/Simmy/Latency/OnLatencyArguments.cs b/src/Polly.Core/Simmy/Latency/OnLatencyArguments.cs new file mode 100644 index 00000000000..9a3896d2904 --- /dev/null +++ b/src/Polly.Core/Simmy/Latency/OnLatencyArguments.cs @@ -0,0 +1,30 @@ +namespace Polly.Simmy.Latency; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by the latency chaos strategy to notify that a delayed occurred. +/// +public readonly struct OnLatencyArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The context associated with the execution of a user-provided callback. + /// The latency that was injected. + public OnLatencyArguments(ResilienceContext context, TimeSpan latency) + { + Context = context; + Latency = latency; + } + + /// + /// Gets the ResilienceContext instance. + /// + public ResilienceContext Context { get; } + + /// + /// Gets the latency that was injected. + /// + public TimeSpan Latency { get; } +} diff --git a/src/Polly.Core/Simmy/MonkeyStrategy.TResult.cs b/src/Polly.Core/Simmy/MonkeyStrategy.TResult.cs new file mode 100644 index 00000000000..a73e0d2e040 --- /dev/null +++ b/src/Polly.Core/Simmy/MonkeyStrategy.TResult.cs @@ -0,0 +1,53 @@ +using Polly.Simmy.Utils; + +namespace Polly.Simmy; + +/// +/// This base strategy class is used to simplify the implementation of generic (reactive) +/// strategies by limiting the number of generic types the execute method receives. +/// +/// The type of result this strategy handles. +/// +/// For strategies that handle all result types the generic parameter must be of type . +/// +public abstract class MonkeyStrategy : ResilienceStrategy +{ + private readonly Func _randomizer; + + /// + /// Initializes a new instance of the class. + /// + /// The chaos strategy options. + protected MonkeyStrategy(MonkeyStrategyOptions options) + { + Guard.NotNull(options); + Guard.NotNull(options.Randomizer); + + _randomizer = options.Randomizer; + InjectionRateGenerator = options.InjectionRateGenerator is not null ? options.InjectionRateGenerator : (_) => new(options.InjectionRate); + EnabledGenerator = options.EnabledGenerator is not null ? options.EnabledGenerator : (_) => new(options.Enabled); + } + + /// + /// Gets the injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// + internal Func> InjectionRateGenerator { get; } + + /// + /// Gets a value that indicates whether or not the chaos strategy is enabled for a given execution. + /// + internal Func> EnabledGenerator { get; } + + /// + /// Determines whether or not the chaos strategy should be injected based on the injection rate and enabled flag. + /// + /// The instance. + /// A boolean value that indicates whether or not the chaos strategy should be injected. + /// Use this method before injecting any chaos strategy to evaluate whether a given chaos strategy needs to be injected during the execution. + protected async ValueTask ShouldInjectAsync(ResilienceContext context) + { + return await MonkeyStrategyHelper + .ShouldInjectAsync(context, InjectionRateGenerator, EnabledGenerator, _randomizer) + .ConfigureAwait(false); + } +} diff --git a/src/Polly.Core/Simmy/MonkeyStrategy.cs b/src/Polly.Core/Simmy/MonkeyStrategy.cs new file mode 100644 index 00000000000..173c0462625 --- /dev/null +++ b/src/Polly.Core/Simmy/MonkeyStrategy.cs @@ -0,0 +1,51 @@ +using Polly.Simmy.Utils; + +namespace Polly.Simmy; + +#pragma warning disable CA1031 // Do not catch general exception types +#pragma warning disable S3928 // Custom ArgumentNullException message + +/// +/// Contains common functionality for chaos strategies which intentionally disrupt executions - which monkey around with calls. +/// +public abstract class MonkeyStrategy : ResilienceStrategy +{ + private readonly Func _randomizer; + + /// + /// Initializes a new instance of the class. + /// + /// The chaos strategy options. + protected MonkeyStrategy(MonkeyStrategyOptions options) + { + Guard.NotNull(options); + Guard.NotNull(options.Randomizer); + + _randomizer = options.Randomizer; + InjectionRateGenerator = options.InjectionRateGenerator is not null ? options.InjectionRateGenerator : (_) => new(options.InjectionRate); + EnabledGenerator = options.EnabledGenerator is not null ? options.EnabledGenerator : (_) => new(options.Enabled); + } + + /// + /// Gets the injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// + internal Func> InjectionRateGenerator { get; } + + /// + /// Gets a value that indicates whether or not the chaos strategy is enabled for a given execution. + /// + internal Func> EnabledGenerator { get; } + + /// + /// Determines whether or not the chaos strategy should be injected based on the injection rate and enabled flag. + /// + /// The instance. + /// A boolean value that indicates whether or not the chaos strategy should be injected. + /// Use this method before injecting any chaos strategy to evaluate whether a given chaos strategy needs to be injected during the execution. + protected async ValueTask ShouldInjectAsync(ResilienceContext context) + { + return await MonkeyStrategyHelper + .ShouldInjectAsync(context, InjectionRateGenerator, EnabledGenerator, _randomizer) + .ConfigureAwait(false); + } +} diff --git a/src/Polly.Core/Simmy/MonkeyStrategyConstants.cs b/src/Polly.Core/Simmy/MonkeyStrategyConstants.cs new file mode 100644 index 00000000000..f736d9e4810 --- /dev/null +++ b/src/Polly.Core/Simmy/MonkeyStrategyConstants.cs @@ -0,0 +1,10 @@ +namespace Polly.Simmy; + +internal static class MonkeyStrategyConstants +{ + public const double MinInjectionThreshold = 0; + + public const double MaxInjectionThreshold = 1; + + public const double DefaultInjectionRate = 0.001; +} diff --git a/src/Polly.Core/Simmy/MonkeyStrategyOptions.TResult.cs b/src/Polly.Core/Simmy/MonkeyStrategyOptions.TResult.cs new file mode 100644 index 00000000000..6c7335ada58 --- /dev/null +++ b/src/Polly.Core/Simmy/MonkeyStrategyOptions.TResult.cs @@ -0,0 +1,56 @@ +using System.ComponentModel.DataAnnotations; + +namespace Polly.Simmy; + +#pragma warning disable CS8618 // Required members are not initialized in constructor since this is a DTO, default value is null + +/// +/// The options associated with the . +/// +/// The type of result the monkey strategy handles. +public abstract class MonkeyStrategyOptions : ResilienceStrategyOptions +{ + /// + /// Gets or sets the injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// + /// + /// Defaults to 0.001, meaning one in a thousand executions/0.1%. Either or this property is required. + /// + [Range(MonkeyStrategyConstants.MinInjectionThreshold, MonkeyStrategyConstants.MaxInjectionThreshold)] + public double InjectionRate { get; set; } = MonkeyStrategyConstants.DefaultInjectionRate; + + /// + /// Gets or sets the injection rate generator for a given execution, which the value should be between [0, 1] (inclusive). + /// + /// + /// Defaults to . Either or this property is required. + /// When this property is the is used. + /// + public Func>? InjectionRateGenerator { get; set; } + + /// + /// Gets or sets the enable generator that indicates whether or not the chaos strategy is enabled for a given execution. + /// + /// + /// Defaults to . Either or this property is required. + /// When this property is the is used. + /// + public Func>? EnabledGenerator { get; set; } + + /// + /// Gets or sets a value indicating whether or not the chaos strategy is enabled for a given execution. + /// + /// + /// Defaults to . Either or this property is required. + /// + public bool Enabled { get; set; } + + /// + /// Gets or sets the Randomizer generator instance that is used to evaluate the injection rate. + /// + /// + /// The default randomizer is thread safe and returns values between 0.0 and 1.0. + /// + [Required] + public Func Randomizer { get; set; } = RandomUtil.Instance.NextDouble; +} diff --git a/src/Polly.Core/Simmy/MonkeyStrategyOptions.cs b/src/Polly.Core/Simmy/MonkeyStrategyOptions.cs new file mode 100644 index 00000000000..74361846962 --- /dev/null +++ b/src/Polly.Core/Simmy/MonkeyStrategyOptions.cs @@ -0,0 +1,11 @@ +namespace Polly.Simmy; + +#pragma warning disable CS8618 // Required members are not initialized in constructor since this is a DTO, default value is null + +/// +/// The options associated with the . +/// +public abstract class MonkeyStrategyOptions : MonkeyStrategyOptions +{ +} + diff --git a/src/Polly.Core/Simmy/Outcomes/FaultGeneratorArguments.cs b/src/Polly.Core/Simmy/Outcomes/FaultGeneratorArguments.cs new file mode 100644 index 00000000000..31bf773dac1 --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/FaultGeneratorArguments.cs @@ -0,0 +1,20 @@ +namespace Polly.Simmy.Outcomes; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by the fault chaos strategy to ge the fault that is going to be injected. +/// +public readonly struct FaultGeneratorArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The context associated with the execution of a user-provided callback. + public FaultGeneratorArguments(ResilienceContext context) => Context = context; + + /// + /// Gets the ResilienceContext instance. + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Simmy/Outcomes/FaultStrategyOptions.cs b/src/Polly.Core/Simmy/Outcomes/FaultStrategyOptions.cs new file mode 100644 index 00000000000..902c4155b7d --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/FaultStrategyOptions.cs @@ -0,0 +1,35 @@ +namespace Polly.Simmy.Outcomes; + +#pragma warning disable CS8618 // Required members are not initialized in constructor since this is a DTO, default value is null + +/// +/// Represents the options for the Fault chaos strategy. +/// +public class FaultStrategyOptions : MonkeyStrategyOptions +{ + /// + /// Gets or sets the delegate that's raised when the outcome is injected. + /// + /// + /// Defaults to . + /// + public Func? OnFaultInjected { get; set; } + + /// + /// Gets or sets the outcome generator to be injected for a given execution. + /// + /// + /// Defaults to . Either or this property is required. + /// When this property is the is used. + /// + public Func>? FaultGenerator { get; set; } + + /// + /// Gets or sets the outcome to be injected for a given execution. + /// + /// + /// Defaults to . Either or this property is required. + /// When this property is the is used. + /// + public Exception? Fault { get; set; } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OnFaultInjectedArguments.cs b/src/Polly.Core/Simmy/Outcomes/OnFaultInjectedArguments.cs new file mode 100644 index 00000000000..93513504c57 --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OnFaultInjectedArguments.cs @@ -0,0 +1,30 @@ +namespace Polly.Simmy.Outcomes; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by the fault chaos strategy to notify that an fault was injected. +/// +public readonly struct OnFaultInjectedArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The context associated with the execution of a user-provided callback. + /// The fault that was injected. + public OnFaultInjectedArguments(ResilienceContext context, Exception fault) + { + Context = context; + Fault = fault; + } + + /// + /// Gets the context of this event. + /// + public ResilienceContext Context { get; } + + /// + /// Gets the Outcome that was injected. + /// + public Exception Fault { get; } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OnOutcomeInjectedArguments.cs b/src/Polly.Core/Simmy/Outcomes/OnOutcomeInjectedArguments.cs new file mode 100644 index 00000000000..6a202560b43 --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OnOutcomeInjectedArguments.cs @@ -0,0 +1,31 @@ +namespace Polly.Simmy.Outcomes; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by the outcome chaos strategy to notify that an outcome was injected. +/// +/// The type of the outcome that was injected. +public readonly struct OnOutcomeInjectedArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The context associated with the execution of a user-provided callback. + /// The outcome that was injected. + public OnOutcomeInjectedArguments(ResilienceContext context, Outcome outcome) + { + Context = context; + Outcome = outcome; + } + + /// + /// Gets the context of this event. + /// + public ResilienceContext Context { get; } + + /// + /// Gets the Outcome that was injected. + /// + public Outcome Outcome { get; } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeChaosStrategy.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeChaosStrategy.cs new file mode 100644 index 00000000000..06a13223107 --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeChaosStrategy.cs @@ -0,0 +1,112 @@ +using Polly.Telemetry; + +namespace Polly.Simmy.Outcomes; + +#pragma warning disable S3928 // Custom ArgumentNullException message + +internal class OutcomeChaosStrategy : MonkeyStrategy +{ + private readonly ResilienceStrategyTelemetry _telemetry; + + public OutcomeChaosStrategy(FaultStrategyOptions options, ResilienceStrategyTelemetry telemetry) + : base(options) + { + if (options.Fault is null && options.FaultGenerator is null) + { + throw new InvalidOperationException("Either Fault or FaultGenerator is required."); + } + + _telemetry = telemetry; + Fault = options.Fault; + OnFaultInjected = options.OnFaultInjected; + FaultGenerator = options.FaultGenerator is not null ? options.FaultGenerator : (_) => new(options.Fault); + } + + public OutcomeChaosStrategy(OutcomeStrategyOptions options, ResilienceStrategyTelemetry telemetry) + : base(options) + { + if (options.Outcome is null && options.OutcomeGenerator is null) + { + throw new InvalidOperationException("Either Outcome or OutcomeGenerator is required."); + } + + _telemetry = telemetry; + Outcome = options.Outcome; + OnOutcomeInjected = options.OnOutcomeInjected; + OutcomeGenerator = options.OutcomeGenerator is not null ? options.OutcomeGenerator : (_) => new(options.Outcome); + } + + public Func, ValueTask>? OnOutcomeInjected { get; } + + public Func? OnFaultInjected { get; } + + public Func?>>? OutcomeGenerator { get; } + + public Func>? FaultGenerator { get; } + + public Outcome? Outcome { get; } + + public Exception? Fault { get; } + + protected internal override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) + { + try + { + if (await ShouldInjectAsync(context).ConfigureAwait(context.ContinueOnCapturedContext)) + { + if (FaultGenerator is not null) + { + var fault = await InjectFault(context).ConfigureAwait(context.ContinueOnCapturedContext); + if (fault is not null) + { + return new Outcome(fault); + } + } + else if (OutcomeGenerator is not null) + { + var outcome = await InjectOutcome(context).ConfigureAwait(context.ContinueOnCapturedContext); + return new Outcome(outcome.Value.Result); + } + } + + return await StrategyHelper.ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + catch (OperationCanceledException e) + { + return new Outcome(e); + } + } + + private async ValueTask?> InjectOutcome(ResilienceContext context) + { + var outcome = await OutcomeGenerator!(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); + var args = new OnOutcomeInjectedArguments(context, outcome.Value); + _telemetry.Report(new(ResilienceEventSeverity.Information, OutcomeConstants.OnOutcomeInjectedEvent), context, args); + + if (OnOutcomeInjected is not null) + { + await OnOutcomeInjected(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + + return outcome; + } + + private async ValueTask InjectFault(ResilienceContext context) + { + var fault = await FaultGenerator!(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); + if (fault is null) + { + return null; + } + + var args = new OnFaultInjectedArguments(context, fault); + _telemetry.Report(new(ResilienceEventSeverity.Information, OutcomeConstants.OnFaultInjectedEvent), context, args); + + if (OnFaultInjected is not null) + { + await OnFaultInjected(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + + return fault; + } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeConstants.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeConstants.cs new file mode 100644 index 00000000000..c3fbf42e8d9 --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeConstants.cs @@ -0,0 +1,8 @@ +namespace Polly.Simmy.Outcomes; + +internal static class OutcomeConstants +{ + public const string OnOutcomeInjectedEvent = "OnOutcomeInjected"; + + public const string OnFaultInjectedEvent = "OnFaultInjectedEvent"; +} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeGeneratorArguments.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeGeneratorArguments.cs new file mode 100644 index 00000000000..28a07d74f4a --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeGeneratorArguments.cs @@ -0,0 +1,20 @@ +namespace Polly.Simmy.Outcomes; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Arguments used by the outcome chaos strategy to ge the outcome that is going to be injected. +/// +public readonly struct OutcomeGeneratorArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// The context associated with the execution of a user-provided callback. + public OutcomeGeneratorArguments(ResilienceContext context) => Context = context; + + /// + /// Gets the ResilienceContext instance. + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.TResult.cs b/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.TResult.cs new file mode 100644 index 00000000000..483ccc27164 --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.TResult.cs @@ -0,0 +1,158 @@ +using System.Diagnostics.CodeAnalysis; +using Polly.Simmy.Outcomes; + +namespace Polly.Simmy; + +/// +/// Extension methods for adding outcome to a . +/// +public static partial class OutcomePipelineBuilderExtensions +{ + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The type of result the retry strategy handles. + /// The builder instance. + /// A value that indicates whether or not the chaos strategy is enabled for a given execution. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The exception to inject. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosFault(this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Exception fault) + { + Guard.NotNull(builder); + + builder.AddFaultCore(new FaultStrategyOptions + { + Enabled = enabled, + InjectionRate = injectionRate, + Fault = fault + }); + return builder; + } + + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The type of result the retry strategy handles. + /// The builder instance. + /// A value that indicates whether or not the chaos strategy is enabled for a given execution. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The exception generator delegate. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosFault( + this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Func faultGenerator) + { + Guard.NotNull(builder); + + builder.AddFaultCore(new FaultStrategyOptions + { + Enabled = enabled, + InjectionRate = injectionRate, + FaultGenerator = (_) => new ValueTask(Task.FromResult(faultGenerator())) + }); + return builder; + } + + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The type of result the retry strategy handles. + /// The builder instance. + /// The fault strategy options. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosFault(this ResiliencePipelineBuilder builder, FaultStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + builder.AddFaultCore(options); + return builder; + } + + /// + /// Adds an outcome chaos strategy to the builder. + /// + /// The type of result the retry strategy handles. + /// The builder instance. + /// A value that indicates whether or not the chaos strategy is enabled for a given execution. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The outcome to inject. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosResult(this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, TResult result) + { + Guard.NotNull(builder); + + builder.AddOutcomeCore>(new OutcomeStrategyOptions + { + Enabled = enabled, + InjectionRate = injectionRate, + Outcome = new(result) + }); + return builder; + } + + /// + /// Adds an outcome chaos strategy to the builder. + /// + /// The type of result the retry strategy handles. + /// The builder instance. + /// A value that indicates whether or not the chaos strategy is enabled for a given execution. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The outcome generator delegate. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosResult( + this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Func outcomeGenerator) + { + Guard.NotNull(builder); + + builder.AddOutcomeCore>(new OutcomeStrategyOptions + { + Enabled = enabled, + InjectionRate = injectionRate, + OutcomeGenerator = (_) => new ValueTask?>(Task.FromResult?>(Outcome.FromResult(outcomeGenerator()))) + }); + return builder; + } + + /// + /// Adds an outcome chaos strategy to the builder. + /// + /// The type of result the retry strategy handles. + /// The builder instance. + /// The outcome strategy options. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosResult(this ResiliencePipelineBuilder builder, OutcomeStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + builder.AddOutcomeCore>(options); + return builder; + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + private static void AddOutcomeCore( + this ResiliencePipelineBuilder builder, + OutcomeStrategyOptions options) + { + builder.AddStrategy( + context => new OutcomeChaosStrategy(options, context.Telemetry), + options); + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + private static void AddFaultCore( + this ResiliencePipelineBuilder builder, + FaultStrategyOptions options) + { + builder.AddStrategy( + context => new OutcomeChaosStrategy(options, context.Telemetry), + options); + } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.cs b/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.cs new file mode 100644 index 00000000000..091ad3a3dfe --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OutcomePipelineBuilderExtensions.cs @@ -0,0 +1,81 @@ +using System.Diagnostics.CodeAnalysis; +using Polly.Simmy.Outcomes; + +namespace Polly.Simmy; + +/// +/// Extension methods for adding outcome to a . +/// +public static partial class OutcomePipelineBuilderExtensions +{ + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The builder instance. + /// A value that indicates whether or not the chaos strategy is enabled for a given execution. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The exception to inject. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosFault(this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Exception fault) + { + Guard.NotNull(builder); + + builder.AddFaultCore(new FaultStrategyOptions + { + Enabled = enabled, + InjectionRate = injectionRate, + Fault = fault + }); + return builder; + } + + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The builder instance. + /// A value that indicates whether or not the chaos strategy is enabled for a given execution. + /// The injection rate for a given execution, which the value should be between [0, 1] (inclusive). + /// The exception generator delegate. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosFault( + this ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Func faultGenerator) + { + Guard.NotNull(builder); + + builder.AddFaultCore(new FaultStrategyOptions + { + Enabled = enabled, + InjectionRate = injectionRate, + FaultGenerator = (_) => new ValueTask(Task.FromResult(faultGenerator())) + }); + return builder; + } + + /// + /// Adds a fault chaos strategy to the builder. + /// + /// The builder instance. + /// The fault strategy options. + /// The builder instance with the retry strategy added. + public static ResiliencePipelineBuilder AddChaosFault(this ResiliencePipelineBuilder builder, FaultStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(options); + + builder.AddFaultCore(options); + return builder; + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + private static void AddFaultCore(this ResiliencePipelineBuilder builder, FaultStrategyOptions options) + { + builder.AddStrategy(context => + new OutcomeChaosStrategy( + options, + context.Telemetry), + options); + } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs new file mode 100644 index 00000000000..3194e63b7d0 --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.TResult.cs @@ -0,0 +1,36 @@ +namespace Polly.Simmy.Outcomes; + +#pragma warning disable CS8618 // Required members are not initialized in constructor since this is a DTO, default value is null + +/// +/// 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. + /// + /// + /// Defaults to . Either or this property is required. + /// When this property is the is used. + /// + public Func?>>? OutcomeGenerator { get; set; } + + /// + /// Gets or sets the outcome to be injected for a given execution. + /// + /// + /// Defaults to . Either or this property is required. + /// When this property is the is used. + /// + public Outcome? Outcome { get; set; } +} diff --git a/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.cs b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.cs new file mode 100644 index 00000000000..7a29aa8ef5d --- /dev/null +++ b/src/Polly.Core/Simmy/Outcomes/OutcomeStrategyOptions.cs @@ -0,0 +1,6 @@ +namespace Polly.Simmy.Outcomes; + +/// +public class OutcomeStrategyOptions : OutcomeStrategyOptions +{ +} diff --git a/src/Polly.Core/Simmy/Utils/MonkeyStrategyHelper.cs b/src/Polly.Core/Simmy/Utils/MonkeyStrategyHelper.cs new file mode 100644 index 00000000000..13e8e4e3420 --- /dev/null +++ b/src/Polly.Core/Simmy/Utils/MonkeyStrategyHelper.cs @@ -0,0 +1,49 @@ +namespace Polly.Simmy.Utils; + +internal static class MonkeyStrategyHelper +{ + public static async ValueTask ShouldInjectAsync( + ResilienceContext context, + Func> injectionRateGenerator, + Func> enabledGenerator, + Func randomizer) + { + Guard.NotNull(context); + + // to prevent executing config delegates if token was signaled before to start. + context.CancellationToken.ThrowIfCancellationRequested(); + + if (!await enabledGenerator(new(context)).ConfigureAwait(context.ContinueOnCapturedContext)) + { + return false; + } + + // to prevent executing InjectionRate config delegate if token was signaled on Enabled configuration delegate. + context.CancellationToken.ThrowIfCancellationRequested(); + + double injectionThreshold = await injectionRateGenerator(new(context)).ConfigureAwait(context.ContinueOnCapturedContext); + + // to prevent executing further config delegates if token was signaled on InjectionRate configuration delegate. + context.CancellationToken.ThrowIfCancellationRequested(); + + injectionThreshold = CoerceInjectionThreshold(injectionThreshold); + return randomizer() < injectionThreshold; + } + + private static double CoerceInjectionThreshold(double injectionThreshold) + { + // stryker disable once equality : no means to test this + if (injectionThreshold < MonkeyStrategyConstants.MinInjectionThreshold) + { + return MonkeyStrategyConstants.MinInjectionThreshold; + } + + // stryker disable once equality : no means to test this + if (injectionThreshold > MonkeyStrategyConstants.MaxInjectionThreshold) + { + return MonkeyStrategyConstants.MaxInjectionThreshold; + } + + return injectionThreshold; + } +} diff --git a/src/Polly.Core/Utils/TimeProviderExtensions.cs b/src/Polly.Core/Utils/TimeProviderExtensions.cs index de2d7354056..7e54f24583b 100644 --- a/src/Polly.Core/Utils/TimeProviderExtensions.cs +++ b/src/Polly.Core/Utils/TimeProviderExtensions.cs @@ -35,6 +35,9 @@ public static Task DelayAsync(this TimeProvider timeProvider, TimeSpan delay, Re // the use of Thread.Sleep() here because it is not cancellable and to // simplify the code. Sync-over-async is not a concern here because it // only applies in the case of a resilience event and not on the hot path. + + // re the Sync-over-async I guess that would be a concern when using the LatencyChaosStrategy + // since that's running on the hot path, thoughts? timeProvider.Delay(delay, context.CancellationToken).GetAwaiter().GetResult(); #pragma warning restore CA1849 diff --git a/test/Polly.Core.Tests/Simmy/Behavior/BehaviorActionArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorActionArgumentsTests.cs new file mode 100644 index 00000000000..d357abd4377 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorActionArgumentsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy.Behavior; + +namespace Polly.Core.Tests.Simmy.Behavior; + +public class BehaviorActionArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new BehaviorActionArguments(ResilienceContextPool.Shared.Get()); + args.Context.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosPipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosPipelineBuilderExtensionsTests.cs new file mode 100644 index 00000000000..ba176a5a1b2 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosPipelineBuilderExtensionsTests.cs @@ -0,0 +1,76 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Simmy; +using Polly.Simmy.Behavior; +using Polly.Testing; + +namespace Polly.Core.Tests.Simmy.Behavior; + +public class BehaviorChaosPipelineBuilderExtensionsTests +{ + public static IEnumerable AddBehavior_Ok_Data() + { + var context = ResilienceContextPool.Shared.Get(); + Func behavior = () => new ValueTask(Task.CompletedTask); + yield return new object[] + { + (ResiliencePipelineBuilder builder) => { builder.AddChaosBehavior(true, 0.5, behavior); }, + (BehaviorChaosStrategy strategy) => + { + strategy.Behavior!.Invoke(new(context)).Preserve().GetAwaiter().IsCompleted.Should().BeTrue(); + strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().BeTrue(); + strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(0.5); + } + }; + } + + [Fact] + public void AddBehavior_Shortcut_Option_Ok() + { + var sut = new ResiliencePipelineBuilder().AddChaosBehavior(true, 0.5, () => new ValueTask(Task.CompletedTask)).Build(); + sut.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); + } + + [Fact] + public void AddBehavior_Shortcut_Option_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosBehavior(true, -1, () => new ValueTask(Task.CompletedTask))) + .Should() + .Throw(); + } + + [Fact] + public void AddBehavior_InvalidOptions_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosBehavior(new BehaviorStrategyOptions())) + .Should() + .Throw(); + } + + [Fact] + public void AddBehavior_Options_Ok() + { + var sut = new ResiliencePipelineBuilder() + .AddChaosBehavior(new BehaviorStrategyOptions + { + Enabled = true, + InjectionRate = 1, + BehaviorAction = (_) => new ValueTask(Task.CompletedTask) + }) + .Build(); + + sut.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); + } + + [MemberData(nameof(AddBehavior_Ok_Data))] + [Theory] + internal void AddBehavior_Generic_Options_Ok(Action> configure, Action assert) + { + var builder = new ResiliencePipelineBuilder(); + configure(builder); + + var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType().Subject; + assert(strategy); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosStrategyTests.cs b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosStrategyTests.cs new file mode 100644 index 00000000000..64947e22a85 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorChaosStrategyTests.cs @@ -0,0 +1,175 @@ +using Polly.Simmy.Behavior; +using Polly.Telemetry; + +namespace Polly.Core.Tests.Simmy.Behavior; +public class BehaviorChaosStrategyTests +{ + private readonly ResilienceStrategyTelemetry _telemetry; + private readonly BehaviorStrategyOptions _options; + private readonly List> _args = new(); + + public BehaviorChaosStrategyTests() + { + _telemetry = TestUtilities.CreateResilienceTelemetry(arg => _args.Add(arg)); + _options = new(); + } + + [Fact] + public void Given_not_enabled_should_not_inject_behaviour() + { + var userDelegateExecuted = false; + var injectedBehaviourExecuted = false; + + _options.InjectionRate = 0.6; + _options.Enabled = false; + _options.Randomizer = () => 0.5; + _options.BehaviorAction = (_) => { injectedBehaviourExecuted = true; return default; }; + + var sut = CreateSut(); + sut.Execute(() => { userDelegateExecuted = true; }); + + userDelegateExecuted.Should().BeTrue(); + injectedBehaviourExecuted.Should().BeFalse(); + } + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_should_inject_behaviour() + { + var userDelegateExecuted = false; + var injectedBehaviourExecuted = false; + + _options.InjectionRate = 0.6; + _options.Enabled = true; + _options.Randomizer = () => 0.5; + _options.BehaviorAction = (_) => { injectedBehaviourExecuted = true; return default; }; + + var sut = CreateSut(); + await sut.ExecuteAsync((_) => { userDelegateExecuted = true; return default; }); + + userDelegateExecuted.Should().BeTrue(); + injectedBehaviourExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_ensure_on_behavior_injected_called() + { + var called = false; + var userDelegateExecuted = false; + var injectedBehaviourExecuted = false; + + _options.InjectionRate = 0.6; + _options.Enabled = true; + _options.Randomizer = () => 0.5; + _options.BehaviorAction = (_) => { injectedBehaviourExecuted = true; return default; }; + _options.OnBehaviorInjected = args => + { + args.Context.Should().NotBeNull(); + args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); + called = true; + return default; + }; + + var sut = CreateSut(); + await sut.ExecuteAsync((_) => { userDelegateExecuted = true; return default; }); + + called.Should().BeTrue(); + userDelegateExecuted.Should().BeTrue(); + injectedBehaviourExecuted.Should().BeTrue(); + _args.Should().HaveCount(1); + _args[0].Arguments.Should().BeOfType(); + } + + [Fact] + public async Task Given_enabled_and_randomly_not_within_threshold_should_not_inject_behaviour() + { + var userDelegateExecuted = false; + var injectedBehaviourExecuted = false; + + _options.InjectionRate = 0.4; + _options.Enabled = false; + _options.Randomizer = () => 0.5; + _options.BehaviorAction = (_) => { injectedBehaviourExecuted = true; return default; }; + + var sut = CreateSut(); + await sut.ExecuteAsync((_) => { userDelegateExecuted = true; return default; }); + + userDelegateExecuted.Should().BeTrue(); + injectedBehaviourExecuted.Should().BeFalse(); + } + + [Fact] + public async Task Should_inject_behaviour_before_executing_user_delegate() + { + var userDelegateExecuted = false; + var injectedBehaviourExecuted = false; + + _options.InjectionRate = 0.6; + _options.Enabled = true; + _options.Randomizer = () => 0.5; + _options.BehaviorAction = (_) => + { + userDelegateExecuted.Should().BeFalse(); // Not yet executed at the time the injected behaviour runs. + injectedBehaviourExecuted = true; + return default; + }; + + var sut = CreateSut(); + await sut.ExecuteAsync((_) => { userDelegateExecuted = true; return default; }); + + userDelegateExecuted.Should().BeTrue(); + injectedBehaviourExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Should_not_execute_user_delegate_when_it_was_cancelled_running_the_injected_behavior() + { + var userDelegateExecuted = false; + var injectedBehaviourExecuted = false; + + using var cts = new CancellationTokenSource(); + _options.InjectionRate = 0.6; + _options.Enabled = true; + _options.Randomizer = () => 0.5; + _options.BehaviorAction = (_) => + { + cts.Cancel(); + injectedBehaviourExecuted = true; + return default; + }; + + var sut = CreateSut(); + await sut.Invoking(s => s.ExecuteAsync(async _ => { await Task.CompletedTask; }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + userDelegateExecuted.Should().BeFalse(); + injectedBehaviourExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Should_not_execute_user_delegate_when_it_was_cancelled_running_the_strategy() + { + var userDelegateExecuted = false; + var enabledGeneratorExecuted = false; + + using var cts = new CancellationTokenSource(); + _options.InjectionRate = 0.6; + _options.Randomizer = () => 0.5; + _options.EnabledGenerator = (_) => + { + cts.Cancel(); + enabledGeneratorExecuted = true; + return new ValueTask(true); + }; + + var sut = CreateSut(); + await sut.Invoking(s => s.ExecuteAsync(async _ => { await Task.CompletedTask; }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + userDelegateExecuted.Should().BeFalse(); + enabledGeneratorExecuted.Should().BeTrue(); + } + + private ResiliencePipeline CreateSut() => new BehaviorChaosStrategy(_options, _telemetry).AsPipeline(); +} diff --git a/test/Polly.Core.Tests/Simmy/Behavior/BehaviorConstantsTests.cs b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorConstantsTests.cs new file mode 100644 index 00000000000..e25fffb3fe0 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorConstantsTests.cs @@ -0,0 +1,12 @@ +using Polly.Simmy.Behavior; + +namespace Polly.Core.Tests.Simmy.Behavior; + +public class BehaviorConstantsTests +{ + [Fact] + public void EnsureDefaults() + { + BehaviorConstants.OnBehaviorInjectedEvent.Should().Be("OnBehaviorInjected"); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Behavior/BehaviorStrategyOptionsTests.cs b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorStrategyOptionsTests.cs new file mode 100644 index 00000000000..d0b88c9390b --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Behavior/BehaviorStrategyOptionsTests.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Simmy; +using Polly.Simmy.Behavior; +using Polly.Utils; + +namespace Polly.Core.Tests.Simmy.Behavior; + +public class BehaviorStrategyOptionsTests +{ + [Fact] + public void Ctor_Ok() + { + var sut = new BehaviorStrategyOptions(); + sut.Randomizer.Should().NotBeNull(); + sut.Enabled.Should().BeFalse(); + sut.EnabledGenerator.Should().BeNull(); + sut.InjectionRate.Should().Be(MonkeyStrategyConstants.DefaultInjectionRate); + sut.InjectionRateGenerator.Should().BeNull(); + sut.BehaviorAction.Should().BeNull(); + sut.OnBehaviorInjected.Should().BeNull(); + } + + [Fact] + public void InvalidOptions() + { + var sut = new BehaviorStrategyOptions(); + + sut + .Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid Options"))) + .Should() + .Throw() + .WithMessage(""" + Invalid Options + + Validation Errors: + The BehaviorAction field is required. + """); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Behavior/OnBehaviorInjectedArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Behavior/OnBehaviorInjectedArgumentsTests.cs new file mode 100644 index 00000000000..d4ea4001855 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Behavior/OnBehaviorInjectedArgumentsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy.Behavior; + +namespace Polly.Core.Tests.Simmy.Behavior; + +public class OnBehaviorInjectedArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new OnBehaviorInjectedArguments(ResilienceContextPool.Shared.Get()); + args.Context.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/EnabledGeneratorArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/EnabledGeneratorArgumentsTests.cs new file mode 100644 index 00000000000..78825a85ba1 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/EnabledGeneratorArgumentsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class EnabledGeneratorArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new EnabledGeneratorArguments(ResilienceContextPool.Shared.Get()); + args.Context.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/InjectionRateGeneratorArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/InjectionRateGeneratorArgumentsTests.cs new file mode 100644 index 00000000000..e5202f2115a --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/InjectionRateGeneratorArgumentsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class InjectionRateGeneratorArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new InjectionRateGeneratorArguments(ResilienceContextPool.Shared.Get()); + args.Context.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosPipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosPipelineBuilderExtensionsTests.cs new file mode 100644 index 00000000000..57be2dbcde7 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosPipelineBuilderExtensionsTests.cs @@ -0,0 +1,58 @@ +using Polly.Simmy; +using Polly.Simmy.Latency; +using Polly.Testing; + +namespace Polly.Core.Tests.Simmy.Latency; + +public class LatencyChaosPipelineBuilderExtensionsTests +{ + public static IEnumerable AddLatency_Ok_Data() + { + var context = ResilienceContextPool.Shared.Get(); + Func behavior = () => new ValueTask(Task.CompletedTask); + yield return new object[] + { + (ResiliencePipelineBuilder builder) => { builder.AddChaosLatency(true, 0.5, TimeSpan.FromSeconds(10)); }, + (LatencyChaosStrategy strategy) => + { + strategy.Latency.Should().Be(TimeSpan.FromSeconds(10)); + strategy.LatencyGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(TimeSpan.FromSeconds(10)); + strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().BeTrue(); + strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(0.5); + } + }; + } + + [Fact] + public void AddLatency_Shortcut_Option_Ok() + { + var sut = new ResiliencePipelineBuilder().AddChaosLatency(true, 0.5, TimeSpan.FromSeconds(10)).Build(); + sut.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); + } + + [Fact] + public void AddLatency_Options_Ok() + { + var sut = new ResiliencePipelineBuilder() + .AddChaosLatency(new LatencyStrategyOptions + { + Enabled = true, + InjectionRate = 1, + LatencyGenerator = (_) => new ValueTask(TimeSpan.FromSeconds(30)) + }) + .Build(); + + sut.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); + } + + [MemberData(nameof(AddLatency_Ok_Data))] + [Theory] + internal void AddLatency_Generic_Options_Ok(Action> configure, Action assert) + { + var builder = new ResiliencePipelineBuilder(); + configure(builder); + + var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType().Subject; + assert(strategy); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosStrategyTests.cs b/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosStrategyTests.cs new file mode 100644 index 00000000000..10b733b4f52 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Latency/LatencyChaosStrategyTests.cs @@ -0,0 +1,157 @@ +using Microsoft.Extensions.Time.Testing; +using Polly.Simmy.Latency; +using Polly.Telemetry; + +namespace Polly.Core.Tests.Simmy.Latency; + +public class LatencyChaosStrategyTests : IDisposable +{ + private readonly ResilienceStrategyTelemetry _telemetry; + private readonly FakeTimeProvider _timeProvider = new(); + private readonly LatencyStrategyOptions _options; + private readonly CancellationTokenSource _cancellationSource; + private readonly TimeSpan _delay = TimeSpan.FromMilliseconds(500); + private readonly List> _args = new(); + + public LatencyChaosStrategyTests() + { + _telemetry = TestUtilities.CreateResilienceTelemetry(arg => _args.Add(arg)); + _options = new LatencyStrategyOptions(); + _cancellationSource = new CancellationTokenSource(); + } + + public void Dispose() => _cancellationSource.Dispose(); + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_should_inject_latency() + { + var userDelegateExecuted = false; + var onLatencyExecuted = false; + + _options.InjectionRate = 0.6; + _options.Enabled = true; + _options.Latency = _delay; + _options.Randomizer = () => 0.5; + _options.OnLatency = args => + { + args.Context.Should().NotBeNull(); + args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); + onLatencyExecuted = true; + return default; + }; + + var before = _timeProvider.GetUtcNow(); + var sut = CreateSut(); + var task = sut.ExecuteAsync(async _ => { userDelegateExecuted = true; await Task.CompletedTask; }); + _timeProvider.Advance(_delay); + await task; + + userDelegateExecuted.Should().BeTrue(); + var after = _timeProvider.GetUtcNow(); + (after - before).Should().Be(_delay); + + _args.Should().HaveCount(1); + _args[0].Arguments.Should().BeOfType(); + onLatencyExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Given_not_enabled_should_not_inject_latency() + { + var userDelegateExecuted = false; + + _options.InjectionRate = 0.6; + _options.Enabled = false; + _options.Latency = _delay; + _options.Randomizer = () => 0.5; + + var before = _timeProvider.GetUtcNow(); + var sut = CreateSut(); + var task = sut.ExecuteAsync(async _ => { userDelegateExecuted = true; await Task.CompletedTask; }); + _timeProvider.Advance(_delay); + await task; + + userDelegateExecuted.Should().BeTrue(); + var after = _timeProvider.GetUtcNow(); + (after - before).Seconds.Should().Be(0); + } + + [Fact] + public async Task Given_enabled_and_randomly_not_within_threshold_should_not_inject_latency() + { + var userDelegateExecuted = false; + + _options.InjectionRate = 0.4; + _options.Enabled = false; + _options.Latency = _delay; + _options.Randomizer = () => 0.5; + + var before = _timeProvider.GetUtcNow(); + var sut = CreateSut(); + var task = sut.ExecuteAsync(async _ => { userDelegateExecuted = true; await Task.CompletedTask; }); + _timeProvider.Advance(_delay); + await task; + + userDelegateExecuted.Should().BeTrue(); + var after = _timeProvider.GetUtcNow(); + (after - before).Seconds.Should().Be(0); + } + + [InlineData(-1000)] + [InlineData(0)] + [Theory] + public async Task Given_latency_is_negative_should_not_inject_latency(double latency) + { + var onLatencyExecuted = false; + var userDelegateExecuted = false; + + _options.InjectionRate = 0.6; + _options.Enabled = true; + _options.Latency = TimeSpan.FromSeconds(latency); + _options.Randomizer = () => 0.5; + + _options.OnLatency = args => + { + args.Context.Should().NotBeNull(); + args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); + onLatencyExecuted = true; + return default; + }; + + var before = _timeProvider.GetUtcNow(); + var sut = CreateSut(); + var task = sut.ExecuteAsync(async _ => { userDelegateExecuted = true; await Task.CompletedTask; }); + _timeProvider.Advance(_delay); + await task; + + userDelegateExecuted.Should().BeTrue(); + var after = _timeProvider.GetUtcNow(); + (after - before).Seconds.Should().Be(0); + onLatencyExecuted.Should().BeFalse(); + } + + [Fact] + public async Task Should_not_execute_user_delegate_when_it_was_cancelled_running_the_strategy() + { + var userDelegateExecuted = false; + + using var cts = new CancellationTokenSource(); + _options.InjectionRate = 0.6; + _options.Enabled = true; + _options.Randomizer = () => 0.5; + _options.LatencyGenerator = (_) => + { + cts.Cancel(); + return new ValueTask(_delay); + }; + + var sut = CreateSut(); + await sut.Invoking(s => s.ExecuteAsync(async _ => { await Task.CompletedTask; }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + userDelegateExecuted.Should().BeFalse(); + } + + private ResiliencePipeline CreateSut() => new LatencyChaosStrategy(_options, _timeProvider, _telemetry).AsPipeline(); +} diff --git a/test/Polly.Core.Tests/Simmy/Latency/LatencyConstantsTests.cs b/test/Polly.Core.Tests/Simmy/Latency/LatencyConstantsTests.cs new file mode 100644 index 00000000000..1d27e28bf8f --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Latency/LatencyConstantsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy.Latency; + +namespace Polly.Core.Tests.Simmy.Latency; + +public class LatencyConstantsTests +{ + [Fact] + public void EnsureDefaults() + { + LatencyConstants.OnLatencyEvent.Should().Be("OnLatency"); + LatencyConstants.DefaultLatency.Should().Be(TimeSpan.FromSeconds(30)); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Latency/LatencyGeneratorArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Latency/LatencyGeneratorArgumentsTests.cs new file mode 100644 index 00000000000..4220a57459a --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Latency/LatencyGeneratorArgumentsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy.Latency; + +namespace Polly.Core.Tests.Simmy.Latency; + +public class LatencyGeneratorArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new LatencyGeneratorArguments(ResilienceContextPool.Shared.Get()); + args.Context.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Latency/LatencyStrategyOptionsTests.cs b/test/Polly.Core.Tests/Simmy/Latency/LatencyStrategyOptionsTests.cs new file mode 100644 index 00000000000..571b5c2df7d --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Latency/LatencyStrategyOptionsTests.cs @@ -0,0 +1,21 @@ +using Polly.Simmy; +using Polly.Simmy.Latency; + +namespace Polly.Core.Tests.Simmy.Latency; + +public class LatencyStrategyOptionsTests +{ + [Fact] + public void Ctor_Ok() + { + var sut = new LatencyStrategyOptions(); + sut.Randomizer.Should().NotBeNull(); + sut.Enabled.Should().BeFalse(); + sut.EnabledGenerator.Should().BeNull(); + sut.InjectionRate.Should().Be(MonkeyStrategyConstants.DefaultInjectionRate); + sut.InjectionRateGenerator.Should().BeNull(); + sut.Latency.Should().Be(LatencyConstants.DefaultLatency); + sut.LatencyGenerator.Should().BeNull(); + sut.OnLatency.Should().BeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Latency/OnLatencyArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Latency/OnLatencyArgumentsTests.cs new file mode 100644 index 00000000000..5c834634e5f --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Latency/OnLatencyArgumentsTests.cs @@ -0,0 +1,14 @@ +using Polly.Simmy.Latency; + +namespace Polly.Core.Tests.Simmy.Latency; + +public class OnLatencyArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new OnLatencyArguments(ResilienceContextPool.Shared.Get(), TimeSpan.FromSeconds(10)); + args.Context.Should().NotBeNull(); + args.Latency.Should().Be(TimeSpan.FromSeconds(10)); + } +} diff --git a/test/Polly.Core.Tests/Simmy/MonkeyStrategyOptionsTTests.cs b/test/Polly.Core.Tests/Simmy/MonkeyStrategyOptionsTTests.cs new file mode 100644 index 00000000000..265bdd5d391 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/MonkeyStrategyOptionsTTests.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Simmy; +using Polly.Utils; + +namespace Polly.Core.Tests.Simmy; +public class MonkeyStrategyOptionsTTests +{ + [Fact] + public void Ctor_Ok() + { + var sut = new TestChaosStrategyOptions(); + + sut.Randomizer.Should().NotBeNull(); + sut.Enabled.Should().BeFalse(); + sut.EnabledGenerator.Should().BeNull(); + sut.InjectionRate.Should().Be(MonkeyStrategyConstants.DefaultInjectionRate); + sut.InjectionRateGenerator.Should().BeNull(); + } + + [InlineData(-1)] + [InlineData(1.1)] + [Theory] + public void InvalidThreshold(double injectionRate) + { + var sut = new TestChaosStrategyOptions + { + InjectionRate = injectionRate, + }; + + sut + .Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid Options"))) + .Should() + .Throw() + .WithMessage(""" + Invalid Options + + Validation Errors: + The field InjectionRate must be between 0 and 1. + """); + } +} diff --git a/test/Polly.Core.Tests/Simmy/MonkeyStrategyOptionsTests.cs b/test/Polly.Core.Tests/Simmy/MonkeyStrategyOptionsTests.cs new file mode 100644 index 00000000000..d768e3b7787 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/MonkeyStrategyOptionsTests.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Simmy; +using Polly.Utils; + +namespace Polly.Core.Tests.Simmy; +public class MonkeyStrategyOptionsTests +{ + [Fact] + public void Ctor_Ok() + { + var sut = new TestChaosStrategyOptions(); + + sut.Randomizer.Should().NotBeNull(); + sut.Enabled.Should().BeFalse(); + sut.EnabledGenerator.Should().BeNull(); + sut.InjectionRate.Should().Be(MonkeyStrategyConstants.DefaultInjectionRate); + sut.InjectionRateGenerator.Should().BeNull(); + } + + [InlineData(-1)] + [InlineData(1.1)] + [Theory] + public void InvalidThreshold(double injectionRate) + { + var sut = new TestChaosStrategyOptions + { + InjectionRate = injectionRate, + }; + + sut + .Invoking(o => ValidationHelper.ValidateObject(new(o, "Invalid Options"))) + .Should() + .Throw() + .WithMessage(""" + Invalid Options + + Validation Errors: + The field InjectionRate must be between 0 and 1. + """); + } +} diff --git a/test/Polly.Core.Tests/Simmy/MonkeyStrategyTTests.cs b/test/Polly.Core.Tests/Simmy/MonkeyStrategyTTests.cs new file mode 100644 index 00000000000..880d1b600b0 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/MonkeyStrategyTTests.cs @@ -0,0 +1,174 @@ +namespace Polly.Core.Tests.Simmy; + +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + +public class MonkeyStrategyTTests +{ + private readonly TestChaosStrategyOptions _options; + + public MonkeyStrategyTTests() => _options = new(); + + [Fact] + public void InvalidCtor() + { + Action act = () => + { + var _ = new TestChaosStrategy(null); + }; + + act.Should().Throw(); + } + + [Fact] + public async Task Ctor_Ok() + { + var context = ResilienceContextPool.Shared.Get(); + _options.EnabledGenerator = (_) => new ValueTask(true); + _options.InjectionRate = 0.5; + + var sut = CreateSut(); + + sut.EnabledGenerator.Should().NotBeNull(); + (await sut.EnabledGenerator(new(context))).Should().BeTrue(); + + sut.InjectionRateGenerator.Should().NotBeNull(); + (await sut.InjectionRateGenerator(new(context))).Should().Be(0.5); + } + + [InlineData(0, false)] + [InlineData(0.5, false)] + [InlineData(-1, false)] + [InlineData(1, true)] + [InlineData(1.1, true)] + [Theory] + public async Task Should_coerce_injection_rate_generator_result_is_not_valid(double injectionRateGeneratorResult, bool shouldBeInjected) + { + var wasMonkeyUnleashed = false; + + _options.EnabledGenerator = (_) => new ValueTask(true); + _options.InjectionRateGenerator = (_) => new ValueTask(injectionRateGeneratorResult); + _options.Randomizer = () => 0.5; + + var sut = CreateSut(); + sut.OnExecute = (_, _) => { wasMonkeyUnleashed = true; return Task.CompletedTask; }; + + await sut.AsPipeline().ExecuteAsync((_) => { return default; }); + + wasMonkeyUnleashed.Should().Be(shouldBeInjected); + } + + [Fact] + public async Task Should_not_inject_chaos_when_it_was_cancelled_before_evaluating_strategy() + { + var wasMonkeyUnleashed = false; + var enableGeneratorExecuted = false; + var injectionRateGeneratorExecuted = false; + + _options.Randomizer = () => 0.5; + _options.EnabledGenerator = (_) => + { + enableGeneratorExecuted = true; + return new ValueTask(true); + }; + _options.InjectionRateGenerator = (_) => + { + injectionRateGeneratorExecuted = true; + return new ValueTask(0.6); + }; + + using var cts = new CancellationTokenSource(); + var sut = CreateSut(); + sut.Before = (_, _) => { cts.Cancel(); }; + + await sut.Invoking(s => s.AsPipeline().ExecuteAsync(async _ => { return await Task.FromResult(5); }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + wasMonkeyUnleashed.Should().BeFalse(); + enableGeneratorExecuted.Should().BeFalse(); + injectionRateGeneratorExecuted.Should().BeFalse(); + } + + [Fact] + public async Task Should_not_inject_chaos_when_it_was_cancelled_on_enable_generator() + { + var wasMonkeyUnleashed = false; + var enableGeneratorExecuted = false; + var injectionRateGeneratorExecuted = false; + + using var cts = new CancellationTokenSource(); + _options.Randomizer = () => 0.5; + _options.EnabledGenerator = (_) => + { + cts.Cancel(); + enableGeneratorExecuted = true; + return new ValueTask(true); + }; + _options.InjectionRateGenerator = (_) => + { + injectionRateGeneratorExecuted = true; + return new ValueTask(0.6); + }; + + var sut = CreateSut(); + + await sut.Invoking(s => s.AsPipeline().ExecuteAsync(async _ => { return await Task.FromResult(5); }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + wasMonkeyUnleashed.Should().BeFalse(); + enableGeneratorExecuted.Should().BeTrue(); + injectionRateGeneratorExecuted.Should().BeFalse(); + } + + [Fact] + public async Task Should_not_inject_chaos_when_it_was_cancelled_on_injection_rate_generator() + { + var wasMonkeyUnleashed = false; + var enableGeneratorExecuted = false; + var injectionRateGeneratorExecuted = false; + + using var cts = new CancellationTokenSource(); + _options.Randomizer = () => 0.5; + _options.EnabledGenerator = (_) => + { + enableGeneratorExecuted = true; + return new ValueTask(true); + }; + _options.InjectionRateGenerator = (_) => + { + cts.Cancel(); + injectionRateGeneratorExecuted = true; + return new ValueTask(0.6); + }; + + var sut = CreateSut(); + + await sut.Invoking(s => s.AsPipeline().ExecuteAsync(async _ => { return await Task.FromResult(5); }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + wasMonkeyUnleashed.Should().BeFalse(); + enableGeneratorExecuted.Should().BeTrue(); + injectionRateGeneratorExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Should_inject_chaos() + { + var wasMonkeyUnleashed = false; + + _options.EnabledGenerator = (_) => new ValueTask(true); + _options.InjectionRate = 0.6; + _options.Randomizer = () => 0.5; + + var sut = CreateSut(); + sut.OnExecute = (_, _) => { wasMonkeyUnleashed = true; return Task.CompletedTask; }; + + await sut.AsPipeline().ExecuteAsync((_) => { return default; }); + + wasMonkeyUnleashed.Should().BeTrue(); + } + + private TestChaosStrategy CreateSut() => new(_options); +} diff --git a/test/Polly.Core.Tests/Simmy/MonkeyStrategyTests.cs b/test/Polly.Core.Tests/Simmy/MonkeyStrategyTests.cs new file mode 100644 index 00000000000..1d40f8875c9 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/MonkeyStrategyTests.cs @@ -0,0 +1,174 @@ +namespace Polly.Core.Tests.Simmy; + +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + +public class MonkeyStrategyTests +{ + private readonly TestChaosStrategyOptions _options; + + public MonkeyStrategyTests() => _options = new(); + + [Fact] + public void InvalidCtor() + { + Action act = () => + { + var _ = new TestChaosStrategy(null); + }; + + act.Should().Throw(); + } + + [Fact] + public async Task Ctor_Ok() + { + var context = ResilienceContextPool.Shared.Get(); + _options.EnabledGenerator = (_) => new ValueTask(true); + _options.InjectionRate = 0.5; + + var sut = CreateSut(); + + sut.EnabledGenerator.Should().NotBeNull(); + (await sut.EnabledGenerator(new(context))).Should().BeTrue(); + + sut.InjectionRateGenerator.Should().NotBeNull(); + (await sut.InjectionRateGenerator(new(context))).Should().Be(0.5); + } + + [InlineData(0, false)] + [InlineData(0.5, false)] + [InlineData(-1, false)] + [InlineData(1, true)] + [InlineData(1.1, true)] + [Theory] + public async Task Should_coerce_injection_rate_generator_result_is_not_valid(double injectionRateGeneratorResult, bool shouldBeInjected) + { + var wasMonkeyUnleashed = false; + + _options.EnabledGenerator = (_) => new ValueTask(true); + _options.InjectionRateGenerator = (_) => new ValueTask(injectionRateGeneratorResult); + _options.Randomizer = () => 0.5; + + var sut = CreateSut(); + sut.OnExecute = (_, _) => { wasMonkeyUnleashed = true; return Task.CompletedTask; }; + + await sut.AsPipeline().ExecuteAsync((_) => { return default; }); + + wasMonkeyUnleashed.Should().Be(shouldBeInjected); + } + + [Fact] + public async Task Should_not_inject_chaos_when_it_was_cancelled_before_evaluating_strategy() + { + var wasMonkeyUnleashed = false; + var enableGeneratorExecuted = false; + var injectionRateGeneratorExecuted = false; + + _options.Randomizer = () => 0.5; + _options.EnabledGenerator = (_) => + { + enableGeneratorExecuted = true; + return new ValueTask(true); + }; + _options.InjectionRateGenerator = (_) => + { + injectionRateGeneratorExecuted = true; + return new ValueTask(0.6); + }; + + using var cts = new CancellationTokenSource(); + var sut = CreateSut(); + sut.Before = (_, _) => { cts.Cancel(); }; + + await sut.Invoking(s => s.AsPipeline().ExecuteAsync(async _ => { await Task.CompletedTask; }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + wasMonkeyUnleashed.Should().BeFalse(); + enableGeneratorExecuted.Should().BeFalse(); + injectionRateGeneratorExecuted.Should().BeFalse(); + } + + [Fact] + public async Task Should_not_inject_chaos_when_it_was_cancelled_on_enable_generator() + { + var wasMonkeyUnleashed = false; + var enableGeneratorExecuted = false; + var injectionRateGeneratorExecuted = false; + + using var cts = new CancellationTokenSource(); + _options.Randomizer = () => 0.5; + _options.EnabledGenerator = (_) => + { + cts.Cancel(); + enableGeneratorExecuted = true; + return new ValueTask(true); + }; + _options.InjectionRateGenerator = (_) => + { + injectionRateGeneratorExecuted = true; + return new ValueTask(0.6); + }; + + var sut = CreateSut(); + + await sut.Invoking(s => s.AsPipeline().ExecuteAsync(async _ => { await Task.CompletedTask; }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + wasMonkeyUnleashed.Should().BeFalse(); + enableGeneratorExecuted.Should().BeTrue(); + injectionRateGeneratorExecuted.Should().BeFalse(); + } + + [Fact] + public async Task Should_not_inject_chaos_when_it_was_cancelled_on_injection_rate_generator() + { + var wasMonkeyUnleashed = false; + var enableGeneratorExecuted = false; + var injectionRateGeneratorExecuted = false; + + using var cts = new CancellationTokenSource(); + _options.Randomizer = () => 0.5; + _options.EnabledGenerator = (_) => + { + enableGeneratorExecuted = true; + return new ValueTask(true); + }; + _options.InjectionRateGenerator = (_) => + { + cts.Cancel(); + injectionRateGeneratorExecuted = true; + return new ValueTask(0.6); + }; + + var sut = CreateSut(); + + await sut.Invoking(s => s.AsPipeline().ExecuteAsync(async _ => { await Task.CompletedTask; }, cts.Token).AsTask()) + .Should() + .ThrowAsync(); + + wasMonkeyUnleashed.Should().BeFalse(); + enableGeneratorExecuted.Should().BeTrue(); + injectionRateGeneratorExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Should_inject_chaos() + { + var wasMonkeyUnleashed = false; + + _options.EnabledGenerator = (_) => new ValueTask(true); + _options.InjectionRate = 0.6; + _options.Randomizer = () => 0.5; + + var sut = CreateSut(); + sut.OnExecute = (_, _) => { wasMonkeyUnleashed = true; return Task.CompletedTask; }; + + await sut.AsPipeline().ExecuteAsync((_) => { return default; }); + + wasMonkeyUnleashed.Should().BeTrue(); + } + + private TestChaosStrategy CreateSut() => new(_options); +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/FaultGeneratorArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/FaultGeneratorArgumentsTests.cs new file mode 100644 index 00000000000..6d05a240390 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/FaultGeneratorArgumentsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy.Outcomes; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class FaultGeneratorArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new FaultGeneratorArguments(ResilienceContextPool.Shared.Get()); + args.Context.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OnFaultInjectedArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OnFaultInjectedArgumentsTests.cs new file mode 100644 index 00000000000..52fec6411ee --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OnFaultInjectedArgumentsTests.cs @@ -0,0 +1,14 @@ +using Polly.Simmy.Outcomes; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class OnFaultInjectedArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new OnFaultInjectedArguments(ResilienceContextPool.Shared.Get(), new InvalidCastException()); + args.Context.Should().NotBeNull(); + args.Fault.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OnOutcomeInjectedArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OnOutcomeInjectedArgumentsTests.cs new file mode 100644 index 00000000000..c2a4bef4549 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OnOutcomeInjectedArgumentsTests.cs @@ -0,0 +1,14 @@ +using Polly.Simmy.Outcomes; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class OnOutcomeInjectedArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new OnOutcomeInjectedArguments(ResilienceContextPool.Shared.Get(), new(200)); + args.Context.Should().NotBeNull(); + args.Outcome.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosPipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosPipelineBuilderExtensionsTests.cs new file mode 100644 index 00000000000..cb3f5ea3d2a --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosPipelineBuilderExtensionsTests.cs @@ -0,0 +1,230 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Simmy; +using Polly.Simmy.Outcomes; +using Polly.Testing; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class OutcomeChaosPipelineBuilderExtensionsTests +{ + public static readonly TheoryData>> ResultStrategy = new() + { + builder => + { + builder.AddChaosResult(new OutcomeStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Outcome = new(100) + }); + + AssertResultStrategy(builder, true, 0.6, new(100)) + .Outcome.Should().Be(new Outcome(100)); + } + }; + + public static readonly TheoryData>> FaultGenericStrategy = new() + { + builder => + { + builder.AddChaosFault(new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Fault = new InvalidOperationException("Dummy exception.") + }); + + AssertFaultStrategy(builder, true, 0.6) + .Fault.Should().BeOfType(typeof(InvalidOperationException)); + } + }; + + public static readonly TheoryData> FaultStrategy = new() + { + builder => + { + builder.AddChaosFault(new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Fault = new InvalidOperationException("Dummy exception.") + }); + + AssertFaultStrategy(builder, true, 0.6) + .Fault.Should().BeOfType(typeof(InvalidOperationException)); + } + }; + + private static OutcomeChaosStrategy AssertResultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate, Outcome outcome) + { + var context = ResilienceContextPool.Shared.Get(); + var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType>().Subject; + + strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(enabled); + strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(injectionRate); + strategy.OutcomeGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(outcome); + + return strategy; + } + + private static OutcomeChaosStrategy AssertFaultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate) + where TException : Exception + { + var context = ResilienceContextPool.Shared.Get(); + var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType>().Subject; + + strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(enabled); + strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(injectionRate); + strategy.FaultGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().BeOfType(typeof(TException)); + + return strategy; + } + + private static OutcomeChaosStrategy AssertFaultStrategy(ResiliencePipelineBuilder builder, bool enabled, double injectionRate) + where TException : Exception + { + var context = ResilienceContextPool.Shared.Get(); + var strategy = builder.Build().GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType>().Subject; + + strategy.EnabledGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(enabled); + strategy.InjectionRateGenerator.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().Be(injectionRate); + strategy.FaultGenerator!.Invoke(new(context)).Preserve().GetAwaiter().GetResult().Should().BeOfType(typeof(TException)); + + return strategy; + } + + [MemberData(nameof(ResultStrategy))] + [Theory] + internal void AddResult_Options_Ok(Action> configure) + { + var builder = new ResiliencePipelineBuilder(); + builder.Invoking(b => configure(b)).Should().NotThrow(); + } + + [MemberData(nameof(FaultGenericStrategy))] + [Theory] + internal void AddFault_Generic_Options_Ok(Action> configure) + { + var builder = new ResiliencePipelineBuilder(); + builder.Invoking(b => configure(b)).Should().NotThrow(); + } + + [MemberData(nameof(FaultStrategy))] + [Theory] + internal void AddFault_Options_Ok(Action configure) + { + var builder = new ResiliencePipelineBuilder(); + builder.Invoking(b => configure(b)).Should().NotThrow(); + } + + [Fact] + public void AddResult_Shortcut_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosResult(true, 0.5, 120) + .Build(); + + AssertResultStrategy(builder, true, 0.5, new(120)); + } + + [Fact] + public void AddResult_Shortcut_Generator_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosResult(true, 0.5, () => 120) + .Build(); + + AssertResultStrategy(builder, true, 0.5, new(120)); + } + + [Fact] + public void AddResult_Shortcut_Option_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosResult(true, -1, () => 120)) + .Should() + .Throw(); + } + + [Fact] + public void AddFault_Shortcut_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosFault(true, 0.5, new InvalidOperationException("Dummy exception")) + .Build(); + + AssertFaultStrategy(builder, true, 0.5); + } + + [Fact] + public void AddFault_Generic_Shortcut_Generator_Option_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosFault( + true, + 1.5, + () => new InvalidOperationException())) + .Should() + .Throw(); + } + + [Fact] + public void AddFault_Shortcut_Generator_Option_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosFault( + true, + 1.5, + () => new InvalidOperationException())) + .Should() + .Throw(); + } + + [Fact] + public void AddFault_Shortcut_Generator_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosFault(true, 0.5, () => new InvalidOperationException("Dummy exception")) + .Build(); + + AssertFaultStrategy(builder, true, 0.5); + } + + [Fact] + public void AddFault_Generic_Shortcut_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosFault(true, 0.5, new InvalidOperationException("Dummy exception")) + .Build(); + + AssertFaultStrategy(builder, true, 0.5); + } + + [Fact] + public void AddFault_Generic_Shortcut_Option_Throws() + { + new ResiliencePipelineBuilder() + .Invoking(b => b.AddChaosFault(true, -1, new InvalidOperationException())) + .Should() + .Throw(); + } + + [Fact] + public void AddFault_Generic_Shortcut_Generator_Option_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder + .AddChaosFault(true, 0.5, () => new InvalidOperationException("Dummy exception")) + .Build(); + + AssertFaultStrategy(builder, true, 0.5); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosStrategyTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosStrategyTests.cs new file mode 100644 index 00000000000..d5f53072285 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeChaosStrategyTests.cs @@ -0,0 +1,416 @@ +using Polly.Simmy; +using Polly.Simmy.Outcomes; +using Polly.Telemetry; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class OutcomeChaosStrategyTests +{ + private readonly ResilienceStrategyTelemetry _telemetry; + private readonly List> _args = new(); + + public OutcomeChaosStrategyTests() => _telemetry = TestUtilities.CreateResilienceTelemetry(arg => _args.Add(arg)); + + public static List FaultCtorTestCases => + new() + { + new object[] { null!, "Value cannot be null. (Parameter 'options')", typeof(ArgumentNullException) }, + new object[] + { + new FaultStrategyOptions + { + InjectionRate = 1, + Enabled = true, + }, + "Either Fault or FaultGenerator is required.", + typeof(InvalidOperationException) + }, + }; + + public static List ResultCtorTestCases => + new() + { + new object[] { null!, "Value cannot be null. (Parameter 'options')", typeof(ArgumentNullException) }, + new object[] + { + new OutcomeStrategyOptions + { + InjectionRate = 1, + Enabled = true, + }, + "Either Outcome or OutcomeGenerator is required.", + typeof(InvalidOperationException) + }, + }; + + [Theory] + [MemberData(nameof(FaultCtorTestCases))] +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + public void FaultInvalidCtor(FaultStrategyOptions options, string expectedMessage, Type expectedException) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + var _ = new OutcomeChaosStrategy(options, _telemetry); + } + catch (Exception ex) + { + Assert.IsType(expectedException, ex); +#if !NET481 + Assert.Equal(expectedMessage, ex.Message); +#endif + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + [Theory] + [MemberData(nameof(ResultCtorTestCases))] +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + public void ResultInvalidCtor(OutcomeStrategyOptions options, string expectedMessage, Type expectedException) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + var _ = new OutcomeChaosStrategy(options, _telemetry); + } + catch (Exception ex) + { + Assert.IsType(expectedException, ex); +#if !NET481 + Assert.Equal(expectedMessage, ex.Message); +#endif + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + [Fact] + public void Given_not_enabled_should_not_inject_fault() + { + var userDelegateExecuted = false; + var fault = new InvalidOperationException("Dummy exception"); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = false, + Randomizer = () => 0.5, + Fault = fault + }; + + var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); + sut.Execute(() => { userDelegateExecuted = true; }); + + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Given_not_enabled_should_not_inject_fault_and_return_outcome() + { + var userDelegateExecuted = false; + var fault = new InvalidOperationException("Dummy exception"); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = false, + Randomizer = () => 0.5, + Fault = fault + }; + + var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); + var response = await sut.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(HttpStatusCode.OK); + }); + + response.Should().Be(HttpStatusCode.OK); + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_should_inject_fault_instead_returning_outcome() + { + var onFaultInjected = false; + var userDelegateExecuted = false; + var exceptionMessage = "Dummy exception"; + var fault = new InvalidOperationException(exceptionMessage); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Fault = fault, + OnFaultInjected = args => + { + args.Context.Should().NotBeNull(); + args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); + onFaultInjected = true; + return default; + } + }; + + var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); + await sut.Invoking(s => s.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(HttpStatusCode.OK); + }).AsTask()) + .Should() + .ThrowAsync() + .WithMessage(exceptionMessage); + + userDelegateExecuted.Should().BeFalse(); + onFaultInjected.Should().BeTrue(); + } + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_should_inject_fault() + { + var onFaultInjected = false; + var userDelegateExecuted = false; + var exceptionMessage = "Dummy exception"; + var fault = new InvalidOperationException(exceptionMessage); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Fault = fault, + OnFaultInjected = args => + { + args.Context.Should().NotBeNull(); + args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); + onFaultInjected = true; + return default; + } + }; + + var sut = CreateSut(options); + await sut.Invoking(s => s.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(200); + }).AsTask()) + .Should() + .ThrowAsync() + .WithMessage(exceptionMessage); + + userDelegateExecuted.Should().BeFalse(); + _args.Should().HaveCount(1); + _args[0].Arguments.Should().BeOfType(); + _args[0].Event.EventName.Should().Be(OutcomeConstants.OnFaultInjectedEvent); + onFaultInjected.Should().BeTrue(); + } + + [Fact] + public void Given_enabled_and_randomly_not_within_threshold_should_not_inject_fault() + { + var userDelegateExecuted = false; + var fault = new InvalidOperationException("Dummy exception"); + + var options = new FaultStrategyOptions + { + InjectionRate = 0.3, + Enabled = true, + Randomizer = () => 0.5, + Fault = fault + }; + + var sut = CreateSut(options); + var result = sut.Execute(_ => + { + userDelegateExecuted = true; + return 200; + }); + + result.Should().Be(200); + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public void Given_enabled_and_randomly_within_threshold_should_not_inject_fault_when_exception_is_null() + { + var userDelegateExecuted = false; + var options = new FaultStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + FaultGenerator = (_) => new ValueTask(Task.FromResult(null)) + }; + + var sut = new ResiliencePipelineBuilder().AddChaosFault(options).Build(); + sut.Execute(_ => + { + userDelegateExecuted = true; + }); + + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public void Given_not_enabled_should_not_inject_result() + { + var userDelegateExecuted = false; + var fakeResult = HttpStatusCode.TooManyRequests; + + var options = new OutcomeStrategyOptions + { + InjectionRate = 0.6, + Enabled = false, + Randomizer = () => 0.5, + Outcome = new Outcome(fakeResult) + }; + + var sut = CreateSut(options); + var response = sut.Execute(() => { userDelegateExecuted = true; return HttpStatusCode.OK; }); + + response.Should().Be(HttpStatusCode.OK); + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_should_inject_result() + { + var onResultInjected = false; + var userDelegateExecuted = false; + var fakeResult = HttpStatusCode.TooManyRequests; + + var options = new OutcomeStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Outcome = new Outcome(fakeResult), + OnOutcomeInjected = args => + { + args.Context.Should().NotBeNull(); + args.Context.CancellationToken.IsCancellationRequested.Should().BeFalse(); + onResultInjected = true; + return default; + } + }; + + var sut = CreateSut(options); + var response = await sut.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(HttpStatusCode.OK); + }); + + response.Should().Be(fakeResult); + userDelegateExecuted.Should().BeFalse(); + + _args.Should().HaveCount(1); + _args[0].Arguments.Should().BeOfType>(); + _args[0].Event.EventName.Should().Be(OutcomeConstants.OnOutcomeInjectedEvent); + onResultInjected.Should().BeTrue(); + } + + [Fact] + public void Given_enabled_and_randomly_not_within_threshold_should_not_inject_result() + { + var userDelegateExecuted = false; + var fakeResult = HttpStatusCode.TooManyRequests; + + var options = new OutcomeStrategyOptions + { + InjectionRate = 0.3, + Enabled = false, + Randomizer = () => 0.5, + Outcome = new Outcome(fakeResult) + }; + + var sut = CreateSut(options); + var response = sut.Execute(_ => + { + userDelegateExecuted = true; + return HttpStatusCode.OK; + }); + + response.Should().Be(HttpStatusCode.OK); + userDelegateExecuted.Should().BeTrue(); + } + + [Fact] + public async Task Given_enabled_and_randomly_within_threshold_should_inject_result_even_as_null() + { + var userDelegateExecuted = false; + var options = new OutcomeStrategyOptions + { + InjectionRate = 0.6, + Enabled = true, + Randomizer = () => 0.5, + Outcome = Outcome.FromResult(null) + }; + + var sut = CreateSut(options); + var response = await sut.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(HttpStatusCode.OK); + }); + + response.Should().Be(null); + userDelegateExecuted.Should().BeFalse(); + } + + [Fact] + public async Task Should_not_execute_user_delegate_when_it_was_cancelled_running_the_strategy() + { + var userDelegateExecuted = false; + + using var cts = new CancellationTokenSource(); + var options = new OutcomeStrategyOptions + { + InjectionRate = 0.6, + Randomizer = () => 0.5, + EnabledGenerator = (_) => + { + cts.Cancel(); + return new ValueTask(true); + }, + Outcome = Outcome.FromResult(HttpStatusCode.TooManyRequests) + }; + + var sut = CreateSut(options); + await sut.Invoking(s => s.ExecuteAsync(async _ => + { + userDelegateExecuted = true; + return await Task.FromResult(HttpStatusCode.OK); + }, cts.Token) + .AsTask()) + .Should() + .ThrowAsync(); + + userDelegateExecuted.Should().BeFalse(); + } + + private ResiliencePipeline CreateSut(OutcomeStrategyOptions options) => + new OutcomeChaosStrategy(options, _telemetry).AsPipeline(); + + private ResiliencePipeline CreateSut(FaultStrategyOptions options) => + new OutcomeChaosStrategy(options, _telemetry).AsPipeline(); +} + +/// +/// Borrowing this from the actual dotnet standard implementation since it is not available in the net481. +/// +public enum HttpStatusCode +{ + // Summary: + // Equivalent to HTTP status 200. System.Net.HttpStatusCode.OK indicates that the + // request succeeded and that the requested information is in the response. This + // is the most common status code to receive. + OK = 200, + + // Summary: + // Equivalent to HTTP status 429. System.Net.HttpStatusCode.TooManyRequests indicates + // that the user has sent too many requests in a given amount of time. + TooManyRequests = 429, +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeConstantsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeConstantsTests.cs new file mode 100644 index 00000000000..f0427f90cd5 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeConstantsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy.Outcomes; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class OutcomeConstantsTests +{ + [Fact] + public void EnsureDefaults() + { + OutcomeConstants.OnFaultInjectedEvent.Should().Be("OnFaultInjectedEvent"); + OutcomeConstants.OnOutcomeInjectedEvent.Should().Be("OnOutcomeInjected"); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeGeneratorArgumentsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeGeneratorArgumentsTests.cs new file mode 100644 index 00000000000..0f8873eceae --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeGeneratorArgumentsTests.cs @@ -0,0 +1,13 @@ +using Polly.Simmy.Outcomes; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class OutcomeGeneratorArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new OutcomeGeneratorArguments(ResilienceContextPool.Shared.Get()); + args.Context.Should().NotBeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeStrategyOptionsTests.cs b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeStrategyOptionsTests.cs new file mode 100644 index 00000000000..d2442bedd6f --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/Outcomes/OutcomeStrategyOptionsTests.cs @@ -0,0 +1,21 @@ +using Polly.Simmy; +using Polly.Simmy.Outcomes; + +namespace Polly.Core.Tests.Simmy.Outcomes; + +public class OutcomeStrategyOptionsTests +{ + [Fact] + public void Ctor_Ok() + { + var sut = new OutcomeStrategyOptions(); + sut.Randomizer.Should().NotBeNull(); + sut.Enabled.Should().BeFalse(); + sut.EnabledGenerator.Should().BeNull(); + sut.InjectionRate.Should().Be(MonkeyStrategyConstants.DefaultInjectionRate); + sut.InjectionRateGenerator.Should().BeNull(); + sut.Outcome.Should().BeNull(); + sut.OnOutcomeInjected.Should().BeNull(); + sut.OutcomeGenerator.Should().BeNull(); + } +} diff --git a/test/Polly.Core.Tests/Simmy/TestChaosStrategy.TResult.cs b/test/Polly.Core.Tests/Simmy/TestChaosStrategy.TResult.cs new file mode 100644 index 00000000000..2b6b6c0f73e --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/TestChaosStrategy.TResult.cs @@ -0,0 +1,45 @@ +using Polly.Simmy; + +namespace Polly.Core.Tests.Simmy; +public sealed class TestChaosStrategy : MonkeyStrategy +{ + public TestChaosStrategy(TestChaosStrategyOptions options) + : base(options) + { + } + + public Action? Before { get; set; } + + public Action? After { get; set; } + + public Func? OnExecute { get; set; } + + protected internal override async ValueTask> ExecuteCore( + Func>> callback, ResilienceContext context, TState state) + { + Before?.Invoke(context, state); + + try + { + if (await ShouldInjectAsync(context).ConfigureAwait(context.ContinueOnCapturedContext)) + { + if (OnExecute != null) + { + await OnExecute(context, state).ConfigureAwait(false); + } + } + + var result = await callback(context, state).ConfigureAwait(false); + + After?.Invoke(result, null); + + return result; + } + catch (Exception e) + { + After?.Invoke(null, e); + + throw; + } + } +} diff --git a/test/Polly.Core.Tests/Simmy/TestChaosStrategy.cs b/test/Polly.Core.Tests/Simmy/TestChaosStrategy.cs new file mode 100644 index 00000000000..5fab6d3777b --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/TestChaosStrategy.cs @@ -0,0 +1,45 @@ +using Polly.Simmy; + +namespace Polly.Core.Tests.Simmy; +public sealed class TestChaosStrategy : MonkeyStrategy +{ + public TestChaosStrategy(TestChaosStrategyOptions options) + : base(options) + { + } + + public Action? Before { get; set; } + + public Action? After { get; set; } + + public Func? OnExecute { get; set; } + + protected internal override async ValueTask> ExecuteCore( + Func>> callback, ResilienceContext context, TState state) + { + Before?.Invoke(context, state); + + try + { + if (await ShouldInjectAsync(context).ConfigureAwait(context.ContinueOnCapturedContext)) + { + if (OnExecute != null) + { + await OnExecute(context, state).ConfigureAwait(false); + } + } + + var result = await callback(context, state).ConfigureAwait(false); + + After?.Invoke(result, null); + + return result; + } + catch (Exception e) + { + After?.Invoke(null, e); + + throw; + } + } +} diff --git a/test/Polly.Core.Tests/Simmy/TestChaosStrategyOptions.TResult.cs b/test/Polly.Core.Tests/Simmy/TestChaosStrategyOptions.TResult.cs new file mode 100644 index 00000000000..e6058581220 --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/TestChaosStrategyOptions.TResult.cs @@ -0,0 +1,7 @@ +using Polly.Simmy; + +namespace Polly.Core.Tests.Simmy; + +public sealed class TestChaosStrategyOptions : MonkeyStrategyOptions +{ +} diff --git a/test/Polly.Core.Tests/Simmy/TestChaosStrategyOptions.cs b/test/Polly.Core.Tests/Simmy/TestChaosStrategyOptions.cs new file mode 100644 index 00000000000..8fb71d918ac --- /dev/null +++ b/test/Polly.Core.Tests/Simmy/TestChaosStrategyOptions.cs @@ -0,0 +1,7 @@ +using Polly.Simmy; + +namespace Polly.Core.Tests.Simmy; + +public sealed class TestChaosStrategyOptions : MonkeyStrategyOptions +{ +}