diff --git a/README.md b/README.md index 76e65d840fc..95063d6f785 100644 --- a/README.md +++ b/README.md @@ -459,6 +459,10 @@ Rate limiter strategy throws `RateLimiterRejectedException` if execution is reje For more details, visit the [rate limiter strategy](https://www.pollydocs.org/strategies/rate-limiter) documentation. +## Chaos engineering + +Starting with version `8.3.0`, Polly has integrated [Simmy](https://github.com/Polly-Contrib/Simmy), a chaos engineering library, directly into its core. For more information, please refer to the dedicated [chaos engineering documentation](https://www.pollydocs.org/chaos/). + ## Next steps To learn more about Polly, visit [pollydocs.org][polly-docs]. diff --git a/docs/chaos/behavior.md b/docs/chaos/behavior.md index 31a39d38328..44d07e8dd95 100644 --- a/docs/chaos/behavior.md +++ b/docs/chaos/behavior.md @@ -17,7 +17,7 @@ The behavior chaos strategy is designed to inject custom behaviors into system o ```cs -// To use a custom function to generate the behavior to inject. +// To use a custom delegated for injected behavior var optionsWithBehaviorGenerator = new BehaviorStrategyOptions { BehaviorAction = static args => RestartRedisVM(), @@ -42,7 +42,7 @@ var optionsOnBehaviorInjected = new BehaviorStrategyOptions new ResiliencePipelineBuilder().AddChaosBehavior(optionsWithBehaviorGenerator); new ResiliencePipelineBuilder().AddChaosBehavior(optionsOnBehaviorInjected); -// There are also a handy overload to inject the chaos easily. +// There are also a handy overload to inject the chaos easily new ResiliencePipelineBuilder().AddChaosBehavior(0.05, RestartRedisVM); ``` diff --git a/docs/chaos/fault.md b/docs/chaos/fault.md index 1f7f4182083..93e807c627f 100644 --- a/docs/chaos/fault.md +++ b/docs/chaos/fault.md @@ -17,15 +17,17 @@ The fault chaos strategy is designed to introduce faults (exceptions) into the s ```cs -// 10% of invocations will be randomly affected. +// 10% of invocations will be randomly affected and one of the exceptions will be thrown (equal probability). var optionsBasic = new FaultStrategyOptions { - FaultGenerator = static args => new ValueTask(new InvalidOperationException("Dummy exception")), + FaultGenerator = new FaultGenerator() + .AddException() // Uses default constructor + .AddException(() => new TimeoutException("Chaos timeout injected.")), // Custom exception generator Enabled = true, InjectionRate = 0.1 }; -// To use a custom function to generate the fault to inject. +// To use a custom delegate to generate the fault to be injected var optionsWithFaultGenerator = new FaultStrategyOptions { FaultGenerator = static args => @@ -34,7 +36,9 @@ var optionsWithFaultGenerator = new FaultStrategyOptions { "DataLayer" => new TimeoutException(), "ApplicationLayer" => new InvalidOperationException(), - _ => null // When the fault generator returns null the strategy won't inject any fault and it will just invoke the user's callback + // When the fault generator returns null, the strategy won't inject + // any fault and just invokes the user's callback. + _ => null }; return new ValueTask(exception); @@ -46,7 +50,7 @@ var optionsWithFaultGenerator = new FaultStrategyOptions // To get notifications when a fault is injected var optionsOnFaultInjected = new FaultStrategyOptions { - FaultGenerator = static args => new ValueTask(new InvalidOperationException("Dummy exception")), + FaultGenerator = new FaultGenerator().AddException(), Enabled = true, InjectionRate = 0.1, OnFaultInjected = static args => @@ -60,7 +64,7 @@ var optionsOnFaultInjected = new FaultStrategyOptions new ResiliencePipelineBuilder().AddChaosFault(optionsBasic); new ResiliencePipelineBuilder().AddChaosFault(optionsWithFaultGenerator); -// There are also a couple of handy overloads to inject the chaos easily. +// There are also a couple of handy overloads to inject the chaos easily new ResiliencePipelineBuilder().AddChaosFault(0.1, () => new InvalidOperationException("Dummy exception")); ``` @@ -136,3 +140,53 @@ sequenceDiagram F->>P: Throws injected Fault P->>C: Propagates Exception ``` + +## Generating faults + +To generate a fault, you need to specify a `FaultGenerator` delegate. You have the following options as to how you customize this delegate: + +### Use `FaultGenerator` class to generate faults + +The `FaultGenerator` is convenience API that allows you to specify what faults (exceptions) are to be injected. Additionally, it also allows assigning weight to each registered fault. + + +```cs +new ResiliencePipelineBuilder() + .AddChaosFault(new FaultStrategyOptions + { + // Use FaultGenerator to register exceptions to be injected + FaultGenerator = new FaultGenerator() + .AddException() // Uses default constructor + .AddException(() => new TimeoutException("Chaos timeout injected.")) // Custom exception generator + .AddException(context => CreateExceptionFromContext(context)) // Access the ResilienceContext + .AddException(weight: 50), // Assign weight to the exception, default is 100 + }); +``` + + +### Use delegates to generate faults + +Delegates give you the most flexibility at the expense of slightly more complicated syntax. Delegates also support asynchronous fault generation, if you ever need that possibility. + + +```cs +new ResiliencePipelineBuilder() + .AddChaosFault(new FaultStrategyOptions + { + // The same behavior can be achieved with delegates + FaultGenerator = args => + { + Exception? exception = Random.Shared.Next(350) switch + { + < 100 => new InvalidOperationException(), + < 200 => new TimeoutException("Chaos timeout injected."), + < 300 => CreateExceptionFromContext(args.Context), + < 350 => new TimeoutException(), + _ => null + }; + + return new ValueTask(exception); + } + }); +``` + diff --git a/docs/chaos/index.md b/docs/chaos/index.md index af2034c2ab4..f80451c84da 100644 --- a/docs/chaos/index.md +++ b/docs/chaos/index.md @@ -48,7 +48,7 @@ All the strategies' options implement the [`MonkeyStrategyOptions`](xref:Polly.S | Property | Default Value | Description | |--------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `InjectionRate` | 0.001 ms | A decimal between 0 and 1 inclusive. The strategy will inject the chaos, randomly, that proportion of the time, e.g.: if 0.2, twenty percent of calls will be randomly affected; if 0.01, one percent of calls; if 1, all calls. | +| `InjectionRate` | 0.001 | A decimal between 0 and 1 inclusive. The strategy will inject the chaos, randomly, that proportion of the time, e.g.: if 0.2, twenty percent of calls will be randomly affected; if 0.01, one percent of calls; if 1, all calls. | | `InjectionRateGenerator` | `null` | Generates the injection rate for a given execution, which the value should be between [0, 1] (inclusive). | | `Enabled` | `false` | Determines whether the strategy is enabled or not. | | `EnabledGenerator` | `null` | The generator that indicates whether the chaos strategy is enabled for a given execution. | diff --git a/docs/chaos/latency.md b/docs/chaos/latency.md index 77a243bf3b5..52aee265b56 100644 --- a/docs/chaos/latency.md +++ b/docs/chaos/latency.md @@ -21,7 +21,7 @@ The latency chaos strategy is designed to introduce controlled delays into syste // See https://www.pollydocs.org/chaos/latency#defaults for defaults. var optionsDefault = new LatencyStrategyOptions(); -// 10% of invocations will be randomly affected. +// 10% of invocations will be randomly affected var basicOptions = new LatencyStrategyOptions { Latency = TimeSpan.FromSeconds(30), @@ -29,7 +29,7 @@ var basicOptions = new LatencyStrategyOptions InjectionRate = 0.1 }; -// To use a custom function to generate the latency to inject. +// To use a custom function to generate the latency to inject var optionsWithLatencyGenerator = new LatencyStrategyOptions { LatencyGenerator = static args => @@ -38,7 +38,9 @@ var optionsWithLatencyGenerator = new LatencyStrategyOptions { "DataLayer" => TimeSpan.FromMilliseconds(500), "ApplicationLayer" => TimeSpan.FromSeconds(2), - _ => TimeSpan.Zero // When the latency generator returns Zero the strategy won't inject any delay and it will just invoke the user's callback + // When the latency generator returns Zero, the strategy + // won't inject any delay and just invokes the user's callback. + _ => TimeSpan.Zero }; return new ValueTask(latency); @@ -64,7 +66,7 @@ var optionsOnBehaviorInjected = new LatencyStrategyOptions new ResiliencePipelineBuilder().AddChaosLatency(optionsDefault); new ResiliencePipelineBuilder().AddChaosLatency(optionsWithLatencyGenerator); -// There are also a handy overload to inject the chaos easily. +// There are also a handy overload to inject the chaos easily new ResiliencePipelineBuilder().AddChaosLatency(0.1, TimeSpan.FromSeconds(30)); ``` diff --git a/docs/chaos/result.md b/docs/chaos/result.md index c2ccffdf12c..dd899ed54e4 100644 --- a/docs/chaos/result.md +++ b/docs/chaos/result.md @@ -19,14 +19,13 @@ The outcome chaos strategy is designed to inject or substitute fake results into ```cs -// To use a custom function to generate the result to inject. +// To use OutcomeGenerator to register the results and exceptions to be injected (equal probability) var optionsWithResultGenerator = new OutcomeStrategyOptions { - OutcomeGenerator = static args => - { - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - return new ValueTask?>(Outcome.FromResult(response)); - }, + OutcomeGenerator = new OutcomeGenerator() + .AddResult(() => new HttpResponseMessage(HttpStatusCode.TooManyRequests)) + .AddResult(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)) + .AddException(() => new HttpRequestException("Chaos request exception.")), Enabled = true, InjectionRate = 0.1 }; @@ -34,11 +33,8 @@ var optionsWithResultGenerator = new OutcomeStrategyOptions // To get notifications when a result is injected var optionsOnBehaviorInjected = new OutcomeStrategyOptions { - OutcomeGenerator = static args => - { - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - return new ValueTask?>(Outcome.FromResult(response)); - }, + OutcomeGenerator = new OutcomeGenerator() + .AddResult(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)), Enabled = true, InjectionRate = 0.1, OnOutcomeInjected = static args => @@ -52,7 +48,7 @@ var optionsOnBehaviorInjected = new OutcomeStrategyOptions new ResiliencePipelineBuilder().AddChaosResult(optionsWithResultGenerator); new ResiliencePipelineBuilder().AddChaosResult(optionsOnBehaviorInjected); -// There are also a couple of handy overloads to inject the chaos easily. +// There are also a couple of handy overloads to inject the chaos easily new ResiliencePipelineBuilder().AddChaosResult(0.1, () => new HttpResponseMessage(HttpStatusCode.TooManyRequests)); ``` @@ -136,3 +132,53 @@ sequenceDiagram B->>P: Returns result P->>C: Returns result ``` + +## Generating outcomes + +To generate a faulted outcome (result or exception), you need to specify a `OutcomeGenerator` delegate. You have the following options as to how you customize this delegate: + +### Use `OutcomeGenerator` class to generate outcomes + +The `OutcomeGenerator` is a convenience API that allows you to specify what outcomes (results or exceptions) are to be injected. Additionally, it also allows assigning weight to each registered outcome. + + +```cs +new ResiliencePipelineBuilder() + .AddChaosResult(new OutcomeStrategyOptions + { + // Use OutcomeGenerator to register the results and exceptions to be injected + OutcomeGenerator = new OutcomeGenerator() + .AddResult(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)) // Result generator + .AddResult(() => new HttpResponseMessage(HttpStatusCode.TooManyRequests), weight: 50) // Result generator with weight + .AddResult(context => CreateResultFromContext(context)) // Access the ResilienceContext to create result + .AddException(), // You can also register exceptions + }); +``` + + +### Use delegates to generate faults + +Delegates give you the most flexibility at the expense of slightly more complicated syntax. Delegates also support asynchronous outcome generation, if you ever need that possibility. + + +```cs +new ResiliencePipelineBuilder() + .AddChaosResult(new OutcomeStrategyOptions + { + // The same behavior can be achieved with delegates + OutcomeGenerator = args => + { + Outcome? outcome = Random.Shared.Next(350) switch + { + < 100 => Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)), + < 150 => Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.TooManyRequests)), + < 250 => Outcome.FromResult(CreateResultFromContext(args.Context)), + < 350 => Outcome.FromException(new TimeoutException()), + _ => null + }; + + return ValueTask.FromResult(outcome); + } + }); +``` + diff --git a/docs/toc.yml b/docs/toc.yml index 7ecd8688a22..b4ea0b5ac16 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -47,7 +47,7 @@ - name: Advanced topics expanded: true items: - - name: Telemetry and monitoring + - name: Telemetry href: advanced/telemetry.md - name: Dependency injection href: advanced/dependency-injection.md diff --git a/src/Snippets/Docs/Chaos.Behavior.cs b/src/Snippets/Docs/Chaos.Behavior.cs index c9368463f31..16d729c57b1 100644 --- a/src/Snippets/Docs/Chaos.Behavior.cs +++ b/src/Snippets/Docs/Chaos.Behavior.cs @@ -12,7 +12,7 @@ public static void BehaviorUsage() static ValueTask RestartRedisVM() => ValueTask.CompletedTask; #region chaos-behavior-usage - // To use a custom function to generate the behavior to inject. + // To use a custom delegated for injected behavior var optionsWithBehaviorGenerator = new BehaviorStrategyOptions { BehaviorAction = static args => RestartRedisVM(), @@ -37,7 +37,7 @@ public static void BehaviorUsage() new ResiliencePipelineBuilder().AddChaosBehavior(optionsWithBehaviorGenerator); new ResiliencePipelineBuilder().AddChaosBehavior(optionsOnBehaviorInjected); - // There are also a handy overload to inject the chaos easily. + // There are also a handy overload to inject the chaos easily new ResiliencePipelineBuilder().AddChaosBehavior(0.05, RestartRedisVM); #endregion diff --git a/src/Snippets/Docs/Chaos.Fault.cs b/src/Snippets/Docs/Chaos.Fault.cs index 61ae15ab3ea..65429ab0f06 100644 --- a/src/Snippets/Docs/Chaos.Fault.cs +++ b/src/Snippets/Docs/Chaos.Fault.cs @@ -5,20 +5,24 @@ namespace Snippets.Docs; +#pragma warning disable CA5394 // Do not use insecure randomness + internal static partial class Chaos { public static void FaultUsage() { #region chaos-fault-usage - // 10% of invocations will be randomly affected. + // 10% of invocations will be randomly affected and one of the exceptions will be thrown (equal probability). var optionsBasic = new FaultStrategyOptions { - FaultGenerator = static args => new ValueTask(new InvalidOperationException("Dummy exception")), + FaultGenerator = new FaultGenerator() + .AddException() // Uses default constructor + .AddException(() => new TimeoutException("Chaos timeout injected.")), // Custom exception generator Enabled = true, InjectionRate = 0.1 }; - // To use a custom function to generate the fault to inject. + // To use a custom delegate to generate the fault to be injected var optionsWithFaultGenerator = new FaultStrategyOptions { FaultGenerator = static args => @@ -27,7 +31,9 @@ public static void FaultUsage() { "DataLayer" => new TimeoutException(), "ApplicationLayer" => new InvalidOperationException(), - _ => null // When the fault generator returns null the strategy won't inject any fault and it will just invoke the user's callback + // When the fault generator returns null, the strategy won't inject + // any fault and just invokes the user's callback. + _ => null }; return new ValueTask(exception); @@ -39,7 +45,7 @@ public static void FaultUsage() // To get notifications when a fault is injected var optionsOnFaultInjected = new FaultStrategyOptions { - FaultGenerator = static args => new ValueTask(new InvalidOperationException("Dummy exception")), + FaultGenerator = new FaultGenerator().AddException(), Enabled = true, InjectionRate = 0.1, OnFaultInjected = static args => @@ -53,7 +59,7 @@ public static void FaultUsage() new ResiliencePipelineBuilder().AddChaosFault(optionsBasic); new ResiliencePipelineBuilder().AddChaosFault(optionsWithFaultGenerator); - // There are also a couple of handy overloads to inject the chaos easily. + // There are also a couple of handy overloads to inject the chaos easily new ResiliencePipelineBuilder().AddChaosFault(0.1, () => new InvalidOperationException("Dummy exception")); #endregion @@ -76,4 +82,50 @@ public static void FaultUsage() .Build(); #endregion } + + public static void FaultGenerator() + { + #region chaos-fault-generator-class + + new ResiliencePipelineBuilder() + .AddChaosFault(new FaultStrategyOptions + { + // Use FaultGenerator to register exceptions to be injected + FaultGenerator = new FaultGenerator() + .AddException() // Uses default constructor + .AddException(() => new TimeoutException("Chaos timeout injected.")) // Custom exception generator + .AddException(context => CreateExceptionFromContext(context)) // Access the ResilienceContext + .AddException(weight: 50), // Assign weight to the exception, default is 100 + }); + + #endregion + } + + public static void FaultGeneratorDelegates() + { + #region chaos-fault-generator-delegate + + new ResiliencePipelineBuilder() + .AddChaosFault(new FaultStrategyOptions + { + // The same behavior can be achieved with delegates + FaultGenerator = args => + { + Exception? exception = Random.Shared.Next(350) switch + { + < 100 => new InvalidOperationException(), + < 200 => new TimeoutException("Chaos timeout injected."), + < 300 => CreateExceptionFromContext(args.Context), + < 350 => new TimeoutException(), + _ => null + }; + + return new ValueTask(exception); + } + }); + + #endregion + } + + private static Exception CreateExceptionFromContext(ResilienceContext context) => new InvalidOperationException(); } diff --git a/src/Snippets/Docs/Chaos.Latency.cs b/src/Snippets/Docs/Chaos.Latency.cs index 75fdf174d9e..a42a5c1bdee 100644 --- a/src/Snippets/Docs/Chaos.Latency.cs +++ b/src/Snippets/Docs/Chaos.Latency.cs @@ -15,7 +15,7 @@ public static void LatencyUsage() // See https://www.pollydocs.org/chaos/latency#defaults for defaults. var optionsDefault = new LatencyStrategyOptions(); - // 10% of invocations will be randomly affected. + // 10% of invocations will be randomly affected var basicOptions = new LatencyStrategyOptions { Latency = TimeSpan.FromSeconds(30), @@ -23,7 +23,7 @@ public static void LatencyUsage() InjectionRate = 0.1 }; - // To use a custom function to generate the latency to inject. + // To use a custom function to generate the latency to inject var optionsWithLatencyGenerator = new LatencyStrategyOptions { LatencyGenerator = static args => @@ -32,7 +32,9 @@ public static void LatencyUsage() { "DataLayer" => TimeSpan.FromMilliseconds(500), "ApplicationLayer" => TimeSpan.FromSeconds(2), - _ => TimeSpan.Zero // When the latency generator returns Zero the strategy won't inject any delay and it will just invoke the user's callback + // When the latency generator returns Zero, the strategy + // won't inject any delay and just invokes the user's callback. + _ => TimeSpan.Zero }; return new ValueTask(latency); @@ -58,7 +60,7 @@ public static void LatencyUsage() new ResiliencePipelineBuilder().AddChaosLatency(optionsDefault); new ResiliencePipelineBuilder().AddChaosLatency(optionsWithLatencyGenerator); - // There are also a handy overload to inject the chaos easily. + // There are also a handy overload to inject the chaos easily new ResiliencePipelineBuilder().AddChaosLatency(0.1, TimeSpan.FromSeconds(30)); #endregion diff --git a/src/Snippets/Docs/Chaos.Result.cs b/src/Snippets/Docs/Chaos.Result.cs index cd8161a5488..3d210f148e6 100644 --- a/src/Snippets/Docs/Chaos.Result.cs +++ b/src/Snippets/Docs/Chaos.Result.cs @@ -6,19 +6,20 @@ namespace Snippets.Docs; +#pragma warning disable CA5394 // Do not use insecure randomness + internal static partial class Chaos { public static void ResultUsage() { #region chaos-result-usage - // To use a custom function to generate the result to inject. + // To use OutcomeGenerator to register the results and exceptions to be injected (equal probability) var optionsWithResultGenerator = new OutcomeStrategyOptions { - OutcomeGenerator = static args => - { - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - return new ValueTask?>(Outcome.FromResult(response)); - }, + OutcomeGenerator = new OutcomeGenerator() + .AddResult(() => new HttpResponseMessage(HttpStatusCode.TooManyRequests)) + .AddResult(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)) + .AddException(() => new HttpRequestException("Chaos request exception.")), Enabled = true, InjectionRate = 0.1 }; @@ -26,11 +27,8 @@ public static void ResultUsage() // To get notifications when a result is injected var optionsOnBehaviorInjected = new OutcomeStrategyOptions { - OutcomeGenerator = static args => - { - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - return new ValueTask?>(Outcome.FromResult(response)); - }, + OutcomeGenerator = new OutcomeGenerator() + .AddResult(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)), Enabled = true, InjectionRate = 0.1, OnOutcomeInjected = static args => @@ -44,7 +42,7 @@ public static void ResultUsage() new ResiliencePipelineBuilder().AddChaosResult(optionsWithResultGenerator); new ResiliencePipelineBuilder().AddChaosResult(optionsOnBehaviorInjected); - // There are also a couple of handy overloads to inject the chaos easily. + // There are also a couple of handy overloads to inject the chaos easily new ResiliencePipelineBuilder().AddChaosResult(0.1, () => new HttpResponseMessage(HttpStatusCode.TooManyRequests)); #endregion @@ -75,4 +73,50 @@ public static void ResultUsage() .Build(); #endregion } + + public static void OutcomeGenerator() + { + #region chaos-outcome-generator-class + + new ResiliencePipelineBuilder() + .AddChaosResult(new OutcomeStrategyOptions + { + // Use OutcomeGenerator to register the results and exceptions to be injected + OutcomeGenerator = new OutcomeGenerator() + .AddResult(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)) // Result generator + .AddResult(() => new HttpResponseMessage(HttpStatusCode.TooManyRequests), weight: 50) // Result generator with weight + .AddResult(context => CreateResultFromContext(context)) // Access the ResilienceContext to create result + .AddException(), // You can also register exceptions + }); + + #endregion + } + + public static void OutcomeGeneratorDelegates() + { + #region chaos-outcome-generator-delegate + + new ResiliencePipelineBuilder() + .AddChaosResult(new OutcomeStrategyOptions + { + // The same behavior can be achieved with delegates + OutcomeGenerator = args => + { + Outcome? outcome = Random.Shared.Next(350) switch + { + < 100 => Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)), + < 150 => Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.TooManyRequests)), + < 250 => Outcome.FromResult(CreateResultFromContext(args.Context)), + < 350 => Outcome.FromException(new TimeoutException()), + _ => null + }; + + return ValueTask.FromResult(outcome); + } + }); + + #endregion + } + + private static HttpResponseMessage CreateResultFromContext(ResilienceContext context) => new(HttpStatusCode.TooManyRequests); }