diff --git a/docs/chaos/behavior.md b/docs/chaos/behavior.md index eb162bf1638..78f012a3775 100644 --- a/docs/chaos/behavior.md +++ b/docs/chaos/behavior.md @@ -20,7 +20,7 @@ The behavior chaos strategy is designed to inject custom behaviors into system o // To use a custom delegated for injected behavior var optionsWithBehaviorGenerator = new ChaosBehaviorStrategyOptions { - BehaviorAction = static args => RestartRedisVM(), + BehaviorAction = static args => RestartRedisAsync(args.Context.CancellationToken), Enabled = true, InjectionRate = 0.05 }; @@ -28,7 +28,7 @@ var optionsWithBehaviorGenerator = new ChaosBehaviorStrategyOptions // To get notifications when a behavior is injected var optionsOnBehaviorInjected = new ChaosBehaviorStrategyOptions { - BehaviorAction = static args => RestartRedisVM(), + BehaviorAction = static args => RestartRedisAsync(args.Context.CancellationToken), Enabled = true, InjectionRate = 0.05, OnBehaviorInjected = static args => @@ -43,7 +43,7 @@ new ResiliencePipelineBuilder().AddChaosBehavior(optionsWithBehaviorGenerator); new ResiliencePipelineBuilder().AddChaosBehavior(optionsOnBehaviorInjected); // There are also a handy overload to inject the chaos easily -new ResiliencePipelineBuilder().AddChaosBehavior(0.05, RestartRedisVM); +new ResiliencePipelineBuilder().AddChaosBehavior(0.05, RestartRedisAsync); ``` @@ -62,7 +62,7 @@ var pipeline = new ResiliencePipelineBuilder() }) .AddChaosBehavior(new ChaosBehaviorStrategyOptions // Chaos strategies are usually placed as the last ones in the pipeline { - BehaviorAction = static args => RestartRedisVM(), + BehaviorAction = static args => RestartRedisAsync(args.Context.CancellationToken), Enabled = true, InjectionRate = 0.05 }) diff --git a/docs/chaos/index.md b/docs/chaos/index.md index fcd60dcd239..0d7d4d471d3 100644 --- a/docs/chaos/index.md +++ b/docs/chaos/index.md @@ -7,6 +7,36 @@ ![Simmy](../media/simmy-logo.png) +## Usage + + +```cs +var builder = new ResiliencePipelineBuilder(); + +// First, configure regular resilience strategies +builder + .AddConcurrencyLimiter(10, 100) + .AddRetry(new RetryStrategyOptions { /* configure options */ }) + .AddCircuitBreaker(new CircuitBreakerStrategyOptions { /* configure options */ }) + .AddTimeout(TimeSpan.FromSeconds(5)); + +// Finally, configure chaos strategies if you want to inject chaos. +// These should come after the regular resilience strategies. + +// 2% of invocations will be injected with chaos +const double InjectionRate = 0.02; + +builder + .AddChaosLatency(InjectionRate, TimeSpan.FromMinutes(1)) // Inject a chaos latency to executions + .AddChaosFault(InjectionRate, () => new InvalidOperationException("Injected by chaos strategy!")) // Inject a chaos fault to executions + .AddChaosOutcome(InjectionRate, () => new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)) // Inject a chaos outcome to executions + .AddChaosBehavior(0.001, cancellationToken => RestartRedisAsync(cancellationToken)); // Inject a chaos behavior to executions +``` + + +> [!NOTE] +> It is usual to place the chaos strategy as the last strategy in the resilience pipeline. By placing the chaos strategies as last, they subvert the usual outbound call at the last minute, substituting their fault or adding extra latency, etc. The existing resilience strategies - further out in the `ResiliencePipeline` - still apply, so you can test how the Polly resilience strategies you have configured handle the chaos/faults injected by Simmy. + ## Motivation There are a lot of questions when it comes to chaos engineering and making sure that a system is actually ready to face the worst possible scenarios: @@ -34,14 +64,10 @@ Chaos strategies (formerly known as Monkey strategies) are in essence a [Resilie | Strategy | Reactive | What does the strategy do? | |-------------------------|----------|----------------------------------------------------------------------| | [Fault](fault.md) | No | Injects exceptions in your system. | -| [Result](result.md) | Yes | Substitute results to fake outcomes in your system. | +| [Outcome](outcome.md) | Yes | Injects fake outcomes (results or exceptions) in your system. | | [Latency](latency.md) | No | Injects latency into executions before the calls are made. | | [Behavior](behavior.md) | No | Allows you to inject *any* extra behaviour, before a call is placed. | -## Usage - -It is usual to place the chaos strategy as the last strategy in the resilience pipeline. By placing the chaos strategies as last, they subvert the usual outbound call at the last minute, substituting their fault or adding extra latency, etc. The existing resilience strategies - further out in the `ResiliencePipeline` - still apply, so you can test how the Polly resilience strategies you have configured handle the chaos/faults injected by Simmy. - ## Common options across strategies All the strategies' options implement the [`ChaosStrategyOptions`](xref:Polly.Simmy.ChaosStrategyOptions) class as it contains the basic configuration for every chaos strategy. diff --git a/docs/chaos/outcome.md b/docs/chaos/outcome.md index 87d2c354cca..3203ef92284 100644 --- a/docs/chaos/outcome.md +++ b/docs/chaos/outcome.md @@ -6,7 +6,6 @@ ## About - **Options**: - - [`ChaosOutcomeStrategyOptions`](xref:Polly.Simmy.Outcomes.ChaosOutcomeStrategyOptions) - [`ChaosOutcomeStrategyOptions`](xref:Polly.Simmy.Outcomes.ChaosOutcomeStrategyOptions`1) - **Extensions**: `AddChaosOutcome` - **Strategy Type**: Reactive diff --git a/docs/toc.yml b/docs/toc.yml index b4ea0b5ac16..6f0f230254e 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -37,8 +37,8 @@ items: - name: Fault href: chaos/fault.md - - name: Result - href: chaos/result.md + - name: Outcome + href: chaos/outcome.md - name: Latency href: chaos/latency.md - name: Behavior diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index a4bdba5e66e..9496b0ddbab 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -16,6 +16,16 @@ Polly.Simmy.Behavior.OnBehaviorInjectedArguments.Context.get -> Polly.Resilience Polly.Simmy.Behavior.OnBehaviorInjectedArguments.OnBehaviorInjectedArguments() -> void Polly.Simmy.Behavior.OnBehaviorInjectedArguments.OnBehaviorInjectedArguments(Polly.ResilienceContext! context) -> void Polly.Simmy.ChaosBehaviorPipelineBuilderExtensions +Polly.Simmy.ChaosStrategyOptions.Enabled.get -> bool +Polly.Simmy.ChaosStrategyOptions.Enabled.set -> void +Polly.Simmy.ChaosStrategyOptions.EnabledGenerator.get -> System.Func>? +Polly.Simmy.ChaosStrategyOptions.EnabledGenerator.set -> void +Polly.Simmy.ChaosStrategyOptions.InjectionRate.get -> double +Polly.Simmy.ChaosStrategyOptions.InjectionRate.set -> void +Polly.Simmy.ChaosStrategyOptions.InjectionRateGenerator.get -> System.Func>? +Polly.Simmy.ChaosStrategyOptions.InjectionRateGenerator.set -> void +Polly.Simmy.ChaosStrategyOptions.Randomizer.get -> System.Func! +Polly.Simmy.ChaosStrategyOptions.Randomizer.set -> void Polly.Simmy.EnabledGeneratorArguments Polly.Simmy.EnabledGeneratorArguments.Context.get -> Polly.ResilienceContext! Polly.Simmy.EnabledGeneratorArguments.EnabledGeneratorArguments() -> void @@ -71,18 +81,6 @@ Polly.Simmy.ChaosStrategy.ChaosStrategy(Polly.Simmy.ChaosStrategyOptions! opt Polly.Simmy.ChaosStrategy.ShouldInjectAsync(Polly.ResilienceContext! context) -> System.Threading.Tasks.ValueTask Polly.Simmy.ChaosStrategyOptions Polly.Simmy.ChaosStrategyOptions.ChaosStrategyOptions() -> void -Polly.Simmy.ChaosStrategyOptions -Polly.Simmy.ChaosStrategyOptions.Enabled.get -> bool -Polly.Simmy.ChaosStrategyOptions.Enabled.set -> void -Polly.Simmy.ChaosStrategyOptions.EnabledGenerator.get -> System.Func>? -Polly.Simmy.ChaosStrategyOptions.EnabledGenerator.set -> void -Polly.Simmy.ChaosStrategyOptions.InjectionRate.get -> double -Polly.Simmy.ChaosStrategyOptions.InjectionRate.set -> void -Polly.Simmy.ChaosStrategyOptions.InjectionRateGenerator.get -> System.Func>? -Polly.Simmy.ChaosStrategyOptions.InjectionRateGenerator.set -> void -Polly.Simmy.ChaosStrategyOptions.ChaosStrategyOptions() -> void -Polly.Simmy.ChaosStrategyOptions.Randomizer.get -> System.Func! -Polly.Simmy.ChaosStrategyOptions.Randomizer.set -> void Polly.Simmy.ChaosOutcomePipelineBuilderExtensions Polly.Simmy.Outcomes.OnOutcomeInjectedArguments Polly.Simmy.Outcomes.OnOutcomeInjectedArguments.Context.get -> Polly.ResilienceContext! @@ -106,7 +104,7 @@ Polly.Simmy.Outcomes.ChaosOutcomeStrategyOptions.OnOutcomeInjected.set Polly.Simmy.Outcomes.ChaosOutcomeStrategyOptions.OutcomeGenerator.get -> System.Func?>>! Polly.Simmy.Outcomes.ChaosOutcomeStrategyOptions.OutcomeGenerator.set -> void Polly.Simmy.Outcomes.ChaosOutcomeStrategyOptions.ChaosOutcomeStrategyOptions() -> void -static Polly.Simmy.ChaosBehaviorPipelineBuilderExtensions.AddChaosBehavior(this TBuilder! builder, double injectionRate, System.Func! behavior) -> TBuilder! +static Polly.Simmy.ChaosBehaviorPipelineBuilderExtensions.AddChaosBehavior(this TBuilder! builder, double injectionRate, System.Func! behavior) -> TBuilder! static Polly.Simmy.ChaosBehaviorPipelineBuilderExtensions.AddChaosBehavior(this TBuilder! builder, Polly.Simmy.Behavior.ChaosBehaviorStrategyOptions! options) -> TBuilder! static Polly.Simmy.Fault.FaultGenerator.implicit operator System.Func>!(Polly.Simmy.Fault.FaultGenerator! generator) -> System.Func>! static Polly.Simmy.ChaosFaultPipelineBuilderExtensions.AddChaosFault(this TBuilder! builder, double injectionRate, System.Func! faultGenerator) -> TBuilder! diff --git a/src/Polly.Core/Simmy/Behavior/ChaosBehaviorPipelineBuilderExtensions.cs b/src/Polly.Core/Simmy/Behavior/ChaosBehaviorPipelineBuilderExtensions.cs index 737289dedbf..35eff9cba36 100644 --- a/src/Polly.Core/Simmy/Behavior/ChaosBehaviorPipelineBuilderExtensions.cs +++ b/src/Polly.Core/Simmy/Behavior/ChaosBehaviorPipelineBuilderExtensions.cs @@ -19,7 +19,7 @@ public static class ChaosBehaviorPipelineBuilderExtensions /// The same builder instance. /// Thrown when is . /// Thrown when the options produced from the arguments are invalid. - public static TBuilder AddChaosBehavior(this TBuilder builder, double injectionRate, Func behavior) + public static TBuilder AddChaosBehavior(this TBuilder builder, double injectionRate, Func behavior) where TBuilder : ResiliencePipelineBuilderBase { Guard.NotNull(builder); @@ -28,7 +28,7 @@ public static TBuilder AddChaosBehavior(this TBuilder builder, double { Enabled = true, InjectionRate = injectionRate, - BehaviorAction = (_) => behavior() + BehaviorAction = args => behavior(args.Context.CancellationToken) }); } diff --git a/src/Polly.Core/Simmy/ChaosStrategyOptions.TResult.cs b/src/Polly.Core/Simmy/ChaosStrategyOptions.TResult.cs deleted file mode 100644 index 1cf4c5a63ae..00000000000 --- a/src/Polly.Core/Simmy/ChaosStrategyOptions.TResult.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Polly.Simmy; - -/// -/// The options associated with the . -/// -/// The type of result the chaos strategy handles. -public abstract class ChaosStrategyOptions : 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(ChaosStrategyConstants.MinInjectionThreshold, ChaosStrategyConstants.MaxInjectionThreshold)] - public double InjectionRate { get; set; } = ChaosStrategyConstants.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/ChaosStrategyOptions.cs b/src/Polly.Core/Simmy/ChaosStrategyOptions.cs index 34e3c0a082d..3ceb4008f5d 100644 --- a/src/Polly.Core/Simmy/ChaosStrategyOptions.cs +++ b/src/Polly.Core/Simmy/ChaosStrategyOptions.cs @@ -1,11 +1,53 @@ -namespace Polly.Simmy; +using System.ComponentModel.DataAnnotations; -#pragma warning disable CS8618 // Required members are not initialized in constructor since this is a DTO, default value is null +namespace Polly.Simmy; /// /// The options associated with the . /// -public abstract class ChaosStrategyOptions : ChaosStrategyOptions +public abstract class ChaosStrategyOptions : 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(ChaosStrategyConstants.MinInjectionThreshold, ChaosStrategyConstants.MaxInjectionThreshold)] + public double InjectionRate { get; set; } = ChaosStrategyConstants.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/EnabledGeneratorArguments.cs b/src/Polly.Core/Simmy/EnabledGeneratorArguments.cs index 45b5cf2a464..f7e1fc677c3 100644 --- a/src/Polly.Core/Simmy/EnabledGeneratorArguments.cs +++ b/src/Polly.Core/Simmy/EnabledGeneratorArguments.cs @@ -3,7 +3,7 @@ #pragma warning disable CA1815 // Override equals and operator equals on value types /// -/// Defines the arguments for the . +/// Defines the arguments for the . /// public readonly struct EnabledGeneratorArguments { diff --git a/src/Polly.Core/Simmy/InjectionRateGeneratorArguments.cs b/src/Polly.Core/Simmy/InjectionRateGeneratorArguments.cs index 8e86ce2dda8..9ebf6fd9c28 100644 --- a/src/Polly.Core/Simmy/InjectionRateGeneratorArguments.cs +++ b/src/Polly.Core/Simmy/InjectionRateGeneratorArguments.cs @@ -3,7 +3,7 @@ #pragma warning disable CA1815 // Override equals and operator equals on value types /// -/// Defines the arguments for the . +/// Defines the arguments for the . /// public readonly struct InjectionRateGeneratorArguments { diff --git a/src/Snippets/Docs/Chaos.Behavior.cs b/src/Snippets/Docs/Chaos.Behavior.cs index 314f38a828a..91088a51fc7 100644 --- a/src/Snippets/Docs/Chaos.Behavior.cs +++ b/src/Snippets/Docs/Chaos.Behavior.cs @@ -9,13 +9,11 @@ internal static partial class Chaos { public static void BehaviorUsage() { - static ValueTask RestartRedisVM() => ValueTask.CompletedTask; - #region chaos-behavior-usage // To use a custom delegated for injected behavior var optionsWithBehaviorGenerator = new ChaosBehaviorStrategyOptions { - BehaviorAction = static args => RestartRedisVM(), + BehaviorAction = static args => RestartRedisAsync(args.Context.CancellationToken), Enabled = true, InjectionRate = 0.05 }; @@ -23,7 +21,7 @@ public static void BehaviorUsage() // To get notifications when a behavior is injected var optionsOnBehaviorInjected = new ChaosBehaviorStrategyOptions { - BehaviorAction = static args => RestartRedisVM(), + BehaviorAction = static args => RestartRedisAsync(args.Context.CancellationToken), Enabled = true, InjectionRate = 0.05, OnBehaviorInjected = static args => @@ -38,7 +36,7 @@ public static void BehaviorUsage() new ResiliencePipelineBuilder().AddChaosBehavior(optionsOnBehaviorInjected); // There are also a handy overload to inject the chaos easily - new ResiliencePipelineBuilder().AddChaosBehavior(0.05, RestartRedisVM); + new ResiliencePipelineBuilder().AddChaosBehavior(0.05, RestartRedisAsync); #endregion #region chaos-behavior-execution @@ -53,7 +51,7 @@ public static void BehaviorUsage() }) .AddChaosBehavior(new ChaosBehaviorStrategyOptions // Chaos strategies are usually placed as the last ones in the pipeline { - BehaviorAction = static args => RestartRedisVM(), + BehaviorAction = static args => RestartRedisAsync(args.Context.CancellationToken), Enabled = true, InjectionRate = 0.05 }) diff --git a/src/Snippets/Docs/Chaos.Index.cs b/src/Snippets/Docs/Chaos.Index.cs new file mode 100644 index 00000000000..98e40ecb91b --- /dev/null +++ b/src/Snippets/Docs/Chaos.Index.cs @@ -0,0 +1,39 @@ +using System.Net.Http; +using Polly.CircuitBreaker; +using Polly.Retry; +using Polly.Simmy; + +namespace Snippets.Docs; + +internal static partial class Chaos +{ + public static void Usage() + { + #region chaos-usage + + var builder = new ResiliencePipelineBuilder(); + + // First, configure regular resilience strategies + builder + .AddConcurrencyLimiter(10, 100) + .AddRetry(new RetryStrategyOptions { /* configure options */ }) + .AddCircuitBreaker(new CircuitBreakerStrategyOptions { /* configure options */ }) + .AddTimeout(TimeSpan.FromSeconds(5)); + + // Finally, configure chaos strategies if you want to inject chaos. + // These should come after the regular resilience strategies. + + // 2% of invocations will be injected with chaos + const double InjectionRate = 0.02; + + builder + .AddChaosLatency(InjectionRate, TimeSpan.FromMinutes(1)) // Inject a chaos latency to executions + .AddChaosFault(InjectionRate, () => new InvalidOperationException("Injected by chaos strategy!")) // Inject a chaos fault to executions + .AddChaosOutcome(InjectionRate, () => new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)) // Inject a chaos outcome to executions + .AddChaosBehavior(0.001, cancellationToken => RestartRedisAsync(cancellationToken)); // Inject a chaos behavior to executions + + #endregion + } + + private static ValueTask RestartRedisAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask; +} diff --git a/test/Polly.Core.Tests/Simmy/Behavior/ChaosBehaviorPipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Simmy/Behavior/ChaosBehaviorPipelineBuilderExtensionsTests.cs index aa733c9758f..6c81201634d 100644 --- a/test/Polly.Core.Tests/Simmy/Behavior/ChaosBehaviorPipelineBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Simmy/Behavior/ChaosBehaviorPipelineBuilderExtensionsTests.cs @@ -10,7 +10,7 @@ public class ChaosBehaviorPipelineBuilderExtensionsTests public static IEnumerable AddBehavior_Ok_Data() { var context = ResilienceContextPool.Shared.Get(); - Func behavior = () => new ValueTask(Task.CompletedTask); + Func behavior = _ => default; yield return new object[] { (ResiliencePipelineBuilder builder) => { builder.AddChaosBehavior(0.5, behavior); }, @@ -26,7 +26,7 @@ public static IEnumerable AddBehavior_Ok_Data() [Fact] public void AddBehavior_Shortcut_Option_Ok() { - var sut = new ResiliencePipelineBuilder().AddChaosBehavior(0.5, () => new ValueTask(Task.CompletedTask)).Build(); + var sut = new ResiliencePipelineBuilder().AddChaosBehavior(0.5, _ => default).Build(); sut.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); } @@ -34,7 +34,7 @@ public void AddBehavior_Shortcut_Option_Ok() public void AddBehavior_Shortcut_Option_Throws() { new ResiliencePipelineBuilder() - .Invoking(b => b.AddChaosBehavior(-1, () => new ValueTask(Task.CompletedTask))) + .Invoking(b => b.AddChaosBehavior(-1, _ => default)) .Should() .Throw(); } @@ -56,7 +56,7 @@ public void AddBehavior_Options_Ok() { Enabled = true, InjectionRate = 1, - BehaviorAction = (_) => new ValueTask(Task.CompletedTask) + BehaviorAction = (_) => default }) .Build();