diff --git a/samples/Chaos/Chaos.csproj b/samples/Chaos/Chaos.csproj new file mode 100644 index 0000000000..829f96fb11 --- /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 0000000000..862d45cc4a --- /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 0000000000..86887373cc --- /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 0000000000..78a396e73a --- /dev/null +++ b/samples/Chaos/Program.cs @@ -0,0 +1,77 @@ +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.AddHttpContextAccessor(); + +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 0000000000..05b07f84ba --- /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 0000000000..909bc97915 --- /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 the `https://jsonplaceholder.typicode.com/todos` endpoint. + +To test the application: + +- 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. diff --git a/samples/Chaos/TodoModel.cs b/samples/Chaos/TodoModel.cs new file mode 100644 index 0000000000..c973fa1949 --- /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 0000000000..6fb0fbb741 --- /dev/null +++ b/samples/Chaos/TodosClient.cs @@ -0,0 +1,7 @@ +namespace Chaos; + +public class TodosClient(HttpClient client) +{ + public async Task> GetTodosAsync(CancellationToken cancellationToken) + => await client.GetFromJsonAsync>("/todos", cancellationToken) ?? []; +} diff --git a/samples/Chaos/appsettings.Development.json b/samples/Chaos/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /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 0000000000..10f68b8c8b --- /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 56e59f13ce..4aa2934c25 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 969194a8de..ade0ec3fc6 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