diff --git a/docs/chaos/outcome.md b/docs/chaos/outcome.md index 2c4d6fac8d..ed21508877 100644 --- a/docs/chaos/outcome.md +++ b/docs/chaos/outcome.md @@ -69,7 +69,7 @@ var pipeline = new ResiliencePipelineBuilder() OutcomeGenerator = static args => { var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - return new ValueTask?>(Outcome.FromResult(response)); + return ValueTask.FromResult?>(Outcome.FromResult(response)); }, InjectionRate = 0.1 }) @@ -143,13 +143,13 @@ new ResiliencePipelineBuilder() 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 + .AddResult(context => new HttpResponseMessage(CreateResultFromContext(context))) // Access the ResilienceContext to create result .AddException(), // You can also register exceptions }); ``` -### Use delegates to generate faults +### Use delegates to generate outcomes 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. @@ -159,15 +159,15 @@ new ResiliencePipelineBuilder() .AddChaosOutcome(new ChaosOutcomeStrategyOptions { // The same behavior can be achieved with delegates - OutcomeGenerator = args => + OutcomeGenerator = static 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)), + < 250 => Outcome.FromResult(new HttpResponseMessage(CreateResultFromContext(args.Context))), < 350 => Outcome.FromException(new TimeoutException()), - _ => null + _ => Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.OK)) }; return ValueTask.FromResult(outcome); @@ -175,3 +175,137 @@ new ResiliencePipelineBuilder() }); ``` + +## Anti-patterns + +### Injecting faults (exceptions) + +❌ DON'T + +Use outcome strategies to inject faults in advanced scenarios which you need to inject outcomes using delegates. This is an opinionated anti-pattern since you can consider an exception as a result/outcome, however, there might be undesired implications when doing so. One of these implications is these is to the telemetry events. Events might end up affecting your metrics as the `ChaosOutcomeStrategy` reports both result and exceptions in the same way. This could pose a problem for instrumentation purposes since it's clearer looking for fault injected events to be 100% sure where/when exceptions were injected, rather than have them mixed in the same "bag". + +Another problem is that you end up losing control of how/when to inject outcomes vs. faults. This is because the approach does not allow you to separately control when to inject a fault vs. an outcome. + + +```cs +var pipeline = new ResiliencePipelineBuilder() + .AddChaosOutcome(new ChaosOutcomeStrategyOptions + { + InjectionRate = 0.5, // Same injection rate for both fault and outcome + OutcomeGenerator = static 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(new HttpResponseMessage(CreateResultFromContext(args.Context))), + < 350 => Outcome.FromException(new HttpRequestException("Chaos request exception.")), // ⚠️ Avoid this ⚠️ + _ => Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.OK)) + }; + + return ValueTask.FromResult(outcome); + }, + OnOutcomeInjected = static args => + { + // You might have to put some logic here to determine what kind of output was injected. 😕 + if (args.Outcome.Exception != null) + { + Console.WriteLine($"OnBehaviorInjected, Exception: {args.Outcome.Exception.Message}, Operation: {args.Context.OperationKey}."); + } + else + { + Console.WriteLine($"OnBehaviorInjected, Outcome: {args.Outcome.Result}, Operation: {args.Context.OperationKey}."); + } + + return default; + } + }) + .Build(); +``` + + +✅ DO + +The previous approach is tempting since it looks like less code, but use fault chaos instead as the [`ChaosFaultStrategy`](fault.md) correctly tracks telemetry events as faults, not just as any other outcome. By separating them, you can control the injection rate and enable/disable them separately which gives you more control when it comes to injecting chaos dynamically and in a controlled manner. + + +```cs +var pipeline = new ResiliencePipelineBuilder() + .AddChaosFault(new ChaosFaultStrategyOptions + { + InjectionRate = 0.1, // Different injection rate for faults + EnabledGenerator = static args => ShouldEnableFaults(args.Context), // Different settings might apply to inject faults + FaultGenerator = static args => + { + Exception? exception = RandomThreshold switch + { + >= 250 and < 350 => new HttpRequestException("Chaos request exception."), + _ => null + }; + + return ValueTask.FromResult(exception); + }, + OnFaultInjected = static args => + { + Console.WriteLine($"OnFaultInjected, Exception: {args.Fault.Message}, Operation: {args.Context.OperationKey}."); + return default; + } + }) + .AddChaosOutcome(new ChaosOutcomeStrategyOptions + { + InjectionRate = 0.5, // Different injection rate for outcomes + EnabledGenerator = static args => ShouldEnableOutcome(args.Context), // Different settings might apply to inject outcomes + OutcomeGenerator = static args => + { + HttpStatusCode statusCode = RandomThreshold switch + { + < 100 => HttpStatusCode.InternalServerError, + < 150 => HttpStatusCode.TooManyRequests, + < 250 => CreateResultFromContext(args.Context), + _ => HttpStatusCode.OK + }; + + return ValueTask.FromResult?>(Outcome.FromResult(new HttpResponseMessage(statusCode))); + }, + OnOutcomeInjected = static args => + { + Console.WriteLine($"OnBehaviorInjected, Outcome: {args.Outcome.Result}, Operation: {args.Context.OperationKey}."); + return default; + } + }) + .Build(); +``` + + +❌ DON'T + +Use outcome strategies to inject only faults, use the [`ChaosFaultStrategy`](fault.md) instead. + + +```cs +new ResiliencePipelineBuilder() + .AddChaosOutcome(new ChaosOutcomeStrategyOptions + { + OutcomeGenerator = new OutcomeGenerator() + .AddException(), // ⚠️ Avoid this ⚠️ + }); +``` + + +✅ DO + +Use fault strategies to inject the exception. + + +```cs +new ResiliencePipelineBuilder() + .AddChaosFault(new ChaosFaultStrategyOptions + { + FaultGenerator = new FaultGenerator() + .AddException(), + }); +``` + + +> [!NOTE] +> Even though the outcome strategy is flexible enough to allow you to inject outcomes as well as exceptions without the need to chain a fault strategy in the pipeline, use your judgment when doing so because of the caveats and side effects explained regarding telemetry and injection control. diff --git a/src/Snippets/Docs/Chaos.Outcome.cs b/src/Snippets/Docs/Chaos.Outcome.cs index a8c004a0a8..fe1e184773 100644 --- a/src/Snippets/Docs/Chaos.Outcome.cs +++ b/src/Snippets/Docs/Chaos.Outcome.cs @@ -1,7 +1,8 @@ -using System.Net; +using System.Net; using System.Net.Http; using Polly.Retry; using Polly.Simmy; +using Polly.Simmy.Fault; using Polly.Simmy.Outcomes; namespace Snippets.Docs; @@ -10,6 +11,8 @@ namespace Snippets.Docs; internal static partial class Chaos { + private static readonly int RandomThreshold = Random.Shared.Next(350); + public static void OutcomeUsage() { #region chaos-outcome-usage @@ -63,7 +66,7 @@ public static void OutcomeUsage() OutcomeGenerator = static args => { var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - return new ValueTask?>(Outcome.FromResult(response)); + return ValueTask.FromResult?>(Outcome.FromResult(response)); }, InjectionRate = 0.1 }) @@ -82,7 +85,7 @@ public static void OutcomeGenerator() 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 + .AddResult(context => new HttpResponseMessage(CreateResultFromContext(context))) // Access the ResilienceContext to create result .AddException(), // You can also register exceptions }); @@ -97,15 +100,15 @@ public static void OutcomeGeneratorDelegates() .AddChaosOutcome(new ChaosOutcomeStrategyOptions { // The same behavior can be achieved with delegates - OutcomeGenerator = args => + OutcomeGenerator = static 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)), + < 250 => Outcome.FromResult(new HttpResponseMessage(CreateResultFromContext(args.Context))), < 350 => Outcome.FromException(new TimeoutException()), - _ => null + _ => Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.OK)) }; return ValueTask.FromResult(outcome); @@ -115,5 +118,127 @@ public static void OutcomeGeneratorDelegates() #endregion } - private static HttpResponseMessage CreateResultFromContext(ResilienceContext context) => new(HttpStatusCode.TooManyRequests); + public static void AntiPattern_GeneratorDelegateInjectFault() + { + #region chaos-outcome-anti-pattern-generator-inject-fault + var pipeline = new ResiliencePipelineBuilder() + .AddChaosOutcome(new ChaosOutcomeStrategyOptions + { + InjectionRate = 0.5, // Same injection rate for both fault and outcome + OutcomeGenerator = static 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(new HttpResponseMessage(CreateResultFromContext(args.Context))), + < 350 => Outcome.FromException(new HttpRequestException("Chaos request exception.")), // ⚠️ Avoid this ⚠️ + _ => Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.OK)) + }; + + return ValueTask.FromResult(outcome); + }, + OnOutcomeInjected = static args => + { + // You might have to put some logic here to determine what kind of output was injected. 😕 + if (args.Outcome.Exception != null) + { + Console.WriteLine($"OnBehaviorInjected, Exception: {args.Outcome.Exception.Message}, Operation: {args.Context.OperationKey}."); + } + else + { + Console.WriteLine($"OnBehaviorInjected, Outcome: {args.Outcome.Result}, Operation: {args.Context.OperationKey}."); + } + + return default; + } + }) + .Build(); + + #endregion + } + + public static void Pattern_GeneratorDelegateInjectFaultAndOutcome() + { + #region chaos-outcome-pattern-generator-inject-fault + var pipeline = new ResiliencePipelineBuilder() + .AddChaosFault(new ChaosFaultStrategyOptions + { + InjectionRate = 0.1, // Different injection rate for faults + EnabledGenerator = static args => ShouldEnableFaults(args.Context), // Different settings might apply to inject faults + FaultGenerator = static args => + { + Exception? exception = RandomThreshold switch + { + >= 250 and < 350 => new HttpRequestException("Chaos request exception."), + _ => null + }; + + return ValueTask.FromResult(exception); + }, + OnFaultInjected = static args => + { + Console.WriteLine($"OnFaultInjected, Exception: {args.Fault.Message}, Operation: {args.Context.OperationKey}."); + return default; + } + }) + .AddChaosOutcome(new ChaosOutcomeStrategyOptions + { + InjectionRate = 0.5, // Different injection rate for outcomes + EnabledGenerator = static args => ShouldEnableOutcome(args.Context), // Different settings might apply to inject outcomes + OutcomeGenerator = static args => + { + HttpStatusCode statusCode = RandomThreshold switch + { + < 100 => HttpStatusCode.InternalServerError, + < 150 => HttpStatusCode.TooManyRequests, + < 250 => CreateResultFromContext(args.Context), + _ => HttpStatusCode.OK + }; + + return ValueTask.FromResult?>(Outcome.FromResult(new HttpResponseMessage(statusCode))); + }, + OnOutcomeInjected = static args => + { + Console.WriteLine($"OnBehaviorInjected, Outcome: {args.Outcome.Result}, Operation: {args.Context.OperationKey}."); + return default; + } + }) + .Build(); + #endregion + } + + public static void AntiPattern_OnlyInjectFault() + { + #region chaos-outcome-anti-pattern-only-inject-fault + + new ResiliencePipelineBuilder() + .AddChaosOutcome(new ChaosOutcomeStrategyOptions + { + OutcomeGenerator = new OutcomeGenerator() + .AddException(), // ⚠️ Avoid this ⚠️ + }); + + #endregion + } + + public static void Pattern_OnlyInjectFault() + { + #region chaos-outcome-pattern-only-inject-fault + + new ResiliencePipelineBuilder() + .AddChaosFault(new ChaosFaultStrategyOptions + { + FaultGenerator = new FaultGenerator() + .AddException(), + }); + + #endregion + } + + private static ValueTask ShouldEnableFaults(ResilienceContext context) => ValueTask.FromResult(true); + + private static ValueTask ShouldEnableOutcome(ResilienceContext context) => ValueTask.FromResult(true); + + private static HttpStatusCode CreateResultFromContext(ResilienceContext context) => HttpStatusCode.TooManyRequests; }