Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example for chaos engineering #1956

Merged
merged 3 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions samples/Chaos/Chaos.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Chaos</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Polly.Core" />
</ItemGroup>

</Project>
45 changes: 45 additions & 0 deletions samples/Chaos/ChaosManager.cs
Original file line number Diff line number Diff line change
@@ -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<bool> 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<double> GetInjectionRateAsync(ResilienceContext context)
{
if (environment.IsDevelopment())
{
return ValueTask.FromResult(0.05);
}

if (environment.IsProduction())
{
return ValueTask.FromResult(0.03);
}

return ValueTask.FromResult(0.0);
}
}
13 changes: 13 additions & 0 deletions samples/Chaos/IChaosManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Polly;

namespace Chaos;

/// <summary>
/// Abstraction for controlling chaos injection.
/// </summary>
public interface IChaosManager
{
ValueTask<bool> IsChaosEnabledAsync(ResilienceContext context);

ValueTask<double> GetInjectionRateAsync(ResilienceContext context);
}
77 changes: 77 additions & 0 deletions samples/Chaos/Program.cs
Original file line number Diff line number Diff line change
@@ -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<IChaosManager, ChaosManager>();
services.AddHttpContextAccessor();

var httpClientBuilder = services.AddHttpClient<TodosClient>(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<IChaosManager>();

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<HttpResponseMessage>
{
EnabledGenerator = args => chaosManager.IsChaosEnabledAsync(args.Context),
InjectionRateGenerator = args => chaosManager.GetInjectionRateAsync(args.Context),
OutcomeGenerator = new OutcomeGenerator<HttpResponseMessage>().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();
12 changes: 12 additions & 0 deletions samples/Chaos/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"Chaos": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:62683;http://localhost:62684"
}
}
}
10 changes: 10 additions & 0 deletions samples/Chaos/README.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions samples/Chaos/TodoModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;

namespace Chaos;

public record TodoModel(
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("title")] string Title);
7 changes: 7 additions & 0 deletions samples/Chaos/TodosClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Chaos;

public class TodosClient(HttpClient client)
{
public async Task<IEnumerable<TodoModel>> GetTodosAsync(CancellationToken cancellationToken)
=> await client.GetFromJsonAsync<IEnumerable<TodoModel>>("/todos", cancellationToken) ?? [];
}
8 changes: 8 additions & 0 deletions samples/Chaos/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions samples/Chaos/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
1 change: 1 addition & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 7 additions & 0 deletions samples/Samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down