Skip to main content

Chaos engineering

Kevlar.Chaos is an optional package for deliberately injecting latency, exceptions, typed results, and caller-defined behavior through ordinary shields.

dotnet add package Kevlar.Chaos

Every chaos strategy is disabled by default. Enabling one should be an explicit, reviewable decision. Bound its blast radius with a low rate plus an operation, environment, predicate, or dynamic kill switch. Do not broadly enable chaos in production; start in tests or staging, keep an immediate disable path, monitor injection metrics, and expand gradually.

Inject latency and faults​

using Kevlar;
using Kevlar.Chaos;

var latency = ChaosShield.Latency(options =>
{
options.Enabled = true;
options.Delay = TimeSpan.FromMilliseconds(250);
options.InjectionRate = 0.02;
options.Environment = "staging";
});

var faults = ChaosShield.Fault(options =>
{
options.Enabled = true;
options.ExceptionGenerator = context =>
new(new IOException($"Injected fault in {context.ShieldName}"));
options.Operation = "checkout";
options.InjectionRate = 0.01;
});

using (ChaosScope.Begin(operation: "checkout", environment: "staging"))
{
var result = await Shield.Compose(latency, faults)
.WithName("payments")
.ExecuteAsync(token => LoadUserAsync(id, token), cancellationToken);
}

Latency honors the shield's TimeProvider and caller cancellation token. Fault injection short-circuits the pipeline with the exact configured exception. If no exception is configured, ChaosInjectedException is used.

Inject typed outcomes​

Typed outcome injection is useful for failures represented as values, such as HTTP responses or sentinels. It returns a Shield<TResult>, so normal typed handling and composition remain available:

using Kevlar.Chaos;

var unavailable = ChaosShield.Outcome<int>(options =>
{
options.Enabled = true;
options.Result = -1;
options.InjectionRate = 0.05;
});

var shield = Shield.For<int>()
.WhenResultEquals(-1)
.FallbackTo(0)
.Wrap(unavailable);

var value = await shield.ExecuteAsync(static _ => new ValueTask<int>(42));

Dynamic control and deterministic runs​

Enabled is the primary safety gate. EnabledGenerator can add a live kill switch; InjectionRateGenerator and Predicate receive the current KevlarContext. Fixed configuration is copied when the shield is built, while generators are evaluated on every execution. Every generator returns ValueTask<T> and is awaited. Return new(value) for synchronous work; a generator that actually yields requires ExecuteAsync, and the async-configuration analyzer reports it when used with synchronous Execute.

using Kevlar.Chaos;

var chaosEnabled = true;
var deterministic = ChaosShield.Fault(options =>
{
options.Enabled = true;
options.EnabledGenerator = _ => new(chaosEnabled);
options.Predicate = context => !context.IsSynchronous;
options.InjectionRateGenerator = context =>
new(context.ShieldName == "test-run" ? 0.25 : 0);
options.Seed = 42;
}).WithName("test-run");

A seed makes the random sample sequence reproducible. One shield instance is thread-safe and can be shared, but concurrent callers can consume that deterministic sequence in different orders; use separate seeded shields when each scenario needs its own exact sequence.

Custom behavior​

ChaosShield.Behavior awaits caller-supplied behavior before continuing. It can model effects that are not just a delay or result, and any exception it throws is preserved as the execution failure.

using Kevlar.Chaos;

var behavior = ChaosShield.Behavior(options =>
{
options.Enabled = true;
options.Behavior = context =>
{
Console.WriteLine($"Injecting custom behavior into {context.ShieldName}");
return ValueTask.CompletedTask;
};
});

Observe injections​

OnInjected receives a ChaosEvent immediately before an injection and returns ValueTask; it is awaited before the injection proceeds, and return default; suffices for synchronous work. The event identifies the injection kind, effective rate and sample, operation, environment, and KevlarContext. The context is pooled: copy values inside the callback rather than retaining the context or event. Hook exceptions follow the shared callback-failure contract: Kevlar reports them without replacing the injected outcome.

On .NET 8+, the Kevlar.Chaos meter publishes the kevlar.chaos.injections counter. Its tags are kevlar.chaos.kind plus the available kevlar.shield.name, kevlar.chaos.operation, and kevlar.chaos.environment values. Subscribe with OpenTelemetry using AddMeter(ChaosDiagnostics.MeterName).

For latency tests, attach a FakeTimeProvider with WithTimeProvider and advance virtual time; no real waiting is required.