From 54163735aeecc0887f86a04a47c481422e071950 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Mon, 5 Feb 2024 19:11:49 +0100 Subject: [PATCH 1/3] Add example for chaos engineering --- samples/Chaos/Chaos.csproj | 15 ++++ samples/Chaos/ChaosManager.cs | 45 ++++++++++++ samples/Chaos/IChaosManager.cs | 13 ++++ samples/Chaos/Program.cs | 76 ++++++++++++++++++++ samples/Chaos/Properties/launchSettings.json | 12 ++++ samples/Chaos/README.md | 10 +++ samples/Chaos/TodoModel.cs | 7 ++ samples/Chaos/TodosClient.cs | 12 ++++ samples/Chaos/appsettings.Development.json | 8 +++ samples/Chaos/appsettings.json | 9 +++ samples/README.md | 1 + samples/Samples.sln | 7 ++ 12 files changed, 215 insertions(+) create mode 100644 samples/Chaos/Chaos.csproj create mode 100644 samples/Chaos/ChaosManager.cs create mode 100644 samples/Chaos/IChaosManager.cs create mode 100644 samples/Chaos/Program.cs create mode 100644 samples/Chaos/Properties/launchSettings.json create mode 100644 samples/Chaos/README.md create mode 100644 samples/Chaos/TodoModel.cs create mode 100644 samples/Chaos/TodosClient.cs create mode 100644 samples/Chaos/appsettings.Development.json create mode 100644 samples/Chaos/appsettings.json diff --git a/samples/Chaos/Chaos.csproj b/samples/Chaos/Chaos.csproj new file mode 100644 index 00000000000..829f96fb11b --- /dev/null +++ b/samples/Chaos/Chaos.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + Chaos + + + + + + + + diff --git a/samples/Chaos/ChaosManager.cs b/samples/Chaos/ChaosManager.cs new file mode 100644 index 00000000000..862d45cc4a2 --- /dev/null +++ b/samples/Chaos/ChaosManager.cs @@ -0,0 +1,45 @@ +using Polly; + +namespace Chaos; + +internal class ChaosManager(IWebHostEnvironment environment, IHttpContextAccessor contextAccessor) : IChaosManager +{ + private const string UserQueryParam = "user"; + + private const string TestUser = "test"; + + public ValueTask IsChaosEnabledAsync(ResilienceContext context) + { + if (environment.IsDevelopment()) + { + return ValueTask.FromResult(true); + } + + // This condition is demonstrative and not recommended to use in real apps. + if (environment.IsProduction() && + contextAccessor.HttpContext is { } httpContext && + httpContext.Request.Query.TryGetValue(UserQueryParam, out var values) && + values == TestUser) + { + // Enable chaos for 'test' user even in production + return ValueTask.FromResult(true); + } + + return ValueTask.FromResult(false); + } + + public ValueTask GetInjectionRateAsync(ResilienceContext context) + { + if (environment.IsDevelopment()) + { + return ValueTask.FromResult(0.05); + } + + if (environment.IsProduction()) + { + return ValueTask.FromResult(0.03); + } + + return ValueTask.FromResult(0.0); + } +} \ No newline at end of file diff --git a/samples/Chaos/IChaosManager.cs b/samples/Chaos/IChaosManager.cs new file mode 100644 index 00000000000..86887373cc8 --- /dev/null +++ b/samples/Chaos/IChaosManager.cs @@ -0,0 +1,13 @@ +using Polly; + +namespace Chaos; + +/// +/// Abstraction for controlling chaos injection. +/// +public interface IChaosManager +{ + ValueTask IsChaosEnabledAsync(ResilienceContext context); + + ValueTask GetInjectionRateAsync(ResilienceContext context); +} \ No newline at end of file diff --git a/samples/Chaos/Program.cs b/samples/Chaos/Program.cs new file mode 100644 index 00000000000..c059e2be165 --- /dev/null +++ b/samples/Chaos/Program.cs @@ -0,0 +1,76 @@ +using Chaos; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Resilience; +using Polly; +using Polly.Simmy; +using Polly.Simmy.Fault; +using Polly.Simmy.Latency; +using Polly.Simmy.Outcomes; + +var builder = WebApplication.CreateBuilder(args); +var services = builder.Services; +services.TryAddSingleton(); +services.TryAddSingleton(); + +var httpClientBuilder = services.AddHttpClient(client => client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com")); + +// Configure the standard resilience handler +httpClientBuilder + .AddStandardResilienceHandler() + .Configure(options => + { + // Update attempt timeout to 1 second + options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(1); + + // Update circuit breaker to handle transient errors and InvalidOperationException + options.CircuitBreaker.ShouldHandle = args => args.Outcome switch + { + {} outcome when HttpClientResiliencePredicates.IsTransient(outcome) => PredicateResult.True(), + { Exception: InvalidOperationException } => PredicateResult.True(), + _ => PredicateResult.False() + }; + + // Update retry strategy to handle transient errors and InvalidOperationException + options.Retry.ShouldHandle = args => args.Outcome switch + { + {} outcome when HttpClientResiliencePredicates.IsTransient(outcome) => PredicateResult.True(), + { Exception: InvalidOperationException } => PredicateResult.True(), + _ => PredicateResult.False() + }; + }); + +// Configure the chaos injection +httpClientBuilder.AddResilienceHandler("chaos", (builder, context) => +{ + // Get IChaosManager from dependency injection + var chaosManager = context.ServiceProvider.GetRequiredService(); + + builder + .AddChaosLatency(new ChaosLatencyStrategyOptions + { + EnabledGenerator = args => chaosManager.IsChaosEnabledAsync(args.Context), + InjectionRateGenerator = args => chaosManager.GetInjectionRateAsync(args.Context), + Latency = TimeSpan.FromSeconds(5) + }) + .AddChaosFault(new ChaosFaultStrategyOptions + { + EnabledGenerator = args => chaosManager.IsChaosEnabledAsync(args.Context), + InjectionRateGenerator = args => chaosManager.GetInjectionRateAsync(args.Context), + FaultGenerator = new FaultGenerator().AddException(() => new InvalidOperationException("Chaos strategy injection!")) + }) + .AddChaosOutcome(new ChaosOutcomeStrategyOptions + { + EnabledGenerator = args => chaosManager.IsChaosEnabledAsync(args.Context), + InjectionRateGenerator = args => chaosManager.GetInjectionRateAsync(args.Context), + OutcomeGenerator = new OutcomeGenerator().AddResult(() => new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)) + }); +}); + +// Run the app +var app = builder.Build(); +app.MapGet("/", async (TodosClient client, HttpContext httpContext, CancellationToken cancellationToken) => +{ + return await client.GetTodosAsync(cancellationToken); +}); + +app.Run(); diff --git a/samples/Chaos/Properties/launchSettings.json b/samples/Chaos/Properties/launchSettings.json new file mode 100644 index 00000000000..05b07f84ba8 --- /dev/null +++ b/samples/Chaos/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Chaos": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:62683;http://localhost:62684" + } + } +} diff --git a/samples/Chaos/README.md b/samples/Chaos/README.md new file mode 100644 index 00000000000..9c8e415ae0a --- /dev/null +++ b/samples/Chaos/README.md @@ -0,0 +1,10 @@ +# Chaos Example + +This example demonstrates how to use new [chaos engineering](https://www.pollydocs.org/chaos) tools in Polly to inject chaos into HTTP client communication. +The HTTP client communicates with `https://jsonplaceholder.typicode.com/todos` endpoint. + +To test the application: + +- Run the app using `dotnet run` command. +- Access the root endpoint `https://localhost:62683` and refresh it multiple times. +- Observe the logs in out console window. You should see chaos injection and also mitigation of chaos by resilience strategies. diff --git a/samples/Chaos/TodoModel.cs b/samples/Chaos/TodoModel.cs new file mode 100644 index 00000000000..c973fa1949a --- /dev/null +++ b/samples/Chaos/TodoModel.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace Chaos; + +public record TodoModel( + [property: JsonPropertyName("id")] int Id, + [property: JsonPropertyName("title")] string Title); \ No newline at end of file diff --git a/samples/Chaos/TodosClient.cs b/samples/Chaos/TodosClient.cs new file mode 100644 index 00000000000..834aa5cd29f --- /dev/null +++ b/samples/Chaos/TodosClient.cs @@ -0,0 +1,12 @@ +namespace Chaos; + +public class TodosClient(HttpClient client) +{ + public async Task?> GetTodosAsync(CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/todos"); + using var response = await client.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync>(cancellationToken))!.Take(10); + } +} \ No newline at end of file diff --git a/samples/Chaos/appsettings.Development.json b/samples/Chaos/appsettings.Development.json new file mode 100644 index 00000000000..0c208ae9181 --- /dev/null +++ b/samples/Chaos/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/Chaos/appsettings.json b/samples/Chaos/appsettings.json new file mode 100644 index 00000000000..10f68b8c8b4 --- /dev/null +++ b/samples/Chaos/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/README.md b/samples/README.md index 56e59f13ce0..4aa2934c254 100644 --- a/samples/README.md +++ b/samples/README.md @@ -7,5 +7,6 @@ This repository contains a solution with basic examples demonstrating the creati - [`Retries`](./Retries) - This part explains how to configure a retry resilience strategy. - [`Extensibility`](./Extensibility) - In this part, you can learn how Polly can be extended with custom resilience strategies. - [`DependencyInjection`](./DependencyInjection) - This section demonstrates the integration of Polly with `IServiceCollection`. +- [`Chaos`](./Chaos) - Simple web application that communicates with an external service using HTTP client. It uses chaos strategies to inject chaos into HTTP client calls. These examples are designed as a quick-start guide to Polly. If you wish to explore more advanced scenarios and further enhance your learning, consider visiting the [Polly-Samples](https://github.com/App-vNext/Polly-Samples) repository. diff --git a/samples/Samples.sln b/samples/Samples.sln index 969194a8de1..ade0ec3fc68 100644 --- a/samples/Samples.sln +++ b/samples/Samples.sln @@ -8,6 +8,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets + ..\Directory.Packages.props = ..\Directory.Packages.props README.md = README.md EndProjectSection EndProject @@ -21,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Retries", "Retries\Retries. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection", "DependencyInjection\DependencyInjection.csproj", "{9B8BFE03-4457-4C55-91AD-4096DDE622C3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chaos", "Chaos\Chaos.csproj", "{A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +50,10 @@ Global {9B8BFE03-4457-4C55-91AD-4096DDE622C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B8BFE03-4457-4C55-91AD-4096DDE622C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B8BFE03-4457-4C55-91AD-4096DDE622C3}.Release|Any CPU.Build.0 = Release|Any CPU + {A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A296E17C-B95F-4B15-8B0D-9D6CC0929A1D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From fd080c24376b27b772060a282082277967f22148 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Mon, 5 Feb 2024 21:56:48 +0100 Subject: [PATCH 2/3] PR comments --- samples/Chaos/Program.cs | 3 ++- samples/Chaos/README.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/Chaos/Program.cs b/samples/Chaos/Program.cs index c059e2be165..78a396e73a3 100644 --- a/samples/Chaos/Program.cs +++ b/samples/Chaos/Program.cs @@ -9,8 +9,9 @@ var builder = WebApplication.CreateBuilder(args); var services = builder.Services; + services.TryAddSingleton(); -services.TryAddSingleton(); +services.AddHttpContextAccessor(); var httpClientBuilder = services.AddHttpClient(client => client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com")); diff --git a/samples/Chaos/README.md b/samples/Chaos/README.md index 9c8e415ae0a..909bc979157 100644 --- a/samples/Chaos/README.md +++ b/samples/Chaos/README.md @@ -1,10 +1,10 @@ # Chaos Example This example demonstrates how to use new [chaos engineering](https://www.pollydocs.org/chaos) tools in Polly to inject chaos into HTTP client communication. -The HTTP client communicates with `https://jsonplaceholder.typicode.com/todos` endpoint. +The HTTP client communicates with the `https://jsonplaceholder.typicode.com/todos` endpoint. To test the application: -- Run the app using `dotnet run` command. +- Run the app using the `dotnet run` command. - Access the root endpoint `https://localhost:62683` and refresh it multiple times. - Observe the logs in out console window. You should see chaos injection and also mitigation of chaos by resilience strategies. From 29f1157b03de727f4e6286d1ae52438c6f3c8c9b Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Mon, 5 Feb 2024 22:26:46 +0100 Subject: [PATCH 3/3] Simplify the code --- samples/Chaos/TodosClient.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/samples/Chaos/TodosClient.cs b/samples/Chaos/TodosClient.cs index 834aa5cd29f..6fb0fbb741c 100644 --- a/samples/Chaos/TodosClient.cs +++ b/samples/Chaos/TodosClient.cs @@ -2,11 +2,6 @@ public class TodosClient(HttpClient client) { - public async Task?> GetTodosAsync(CancellationToken cancellationToken) - { - using var request = new HttpRequestMessage(HttpMethod.Get, "/todos"); - using var response = await client.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - return (await response.Content.ReadFromJsonAsync>(cancellationToken))!.Take(10); - } -} \ No newline at end of file + public async Task> GetTodosAsync(CancellationToken cancellationToken) + => await client.GetFromJsonAsync>("/todos", cancellationToken) ?? []; +}