Policy Engine Component¶
Verified by tests
PolicyEngineTests, CircuitBreakerTests, StreamRateLimiterTests, SubscriptionRetryHelperTests — library CI run #31657041675 (2026-08-13)
Updated
Earlier drafts of this page described an attribute-based cross-cutting policy system — [Retry], [Timeout], [Cache], [CircuitBreaker] attributes woven around receptors via an IPolicyOf<T> interface and a generated PolicyWeaver. That system did not ship. What v1.0.0 actually provides is:
IPolicyEngine— a predicate-based routing/configuration policy engine (AddPolicy/MatchAsync). See Policy-Based Routing for the full guide.- Resilience primitives in
Whizbang.Core.Resilience— a programmaticCircuitBreaker<TResult>, aStreamRateLimiter, and subscription retry helpers — plus worker-level retry with exponential backoff viaWorkerRetryOptions.
This page documents those shipped components.
The Policy Engine (IPolicyEngine)¶
The shipped policy engine matches a message's context against ordered, named predicates and returns the first matching policy's configuration:
IPolicyEngine contract
public interface IPolicyEngine {
// Policies are evaluated in the order they are added.
void AddPolicy(
string name,
Func<PolicyContext, bool> predicate,
Action<PolicyConfiguration> configure
);
// Returns the configuration for the first matching policy, or null if no match.
// Records every evaluation in context.Trail.
Task<PolicyConfiguration?> MatchAsync(PolicyContext context);
}
PolicyEngine is the default implementation. It handles routing-shaped concerns — topics, transport publish/subscribe targets, stream ids, execution strategy selection, partitioning, concurrency limits, and persistence-size guards — not retry/timeout/cache wrapping. See Policy-Based Routing for predicates, the fluent PolicyConfiguration API, pooling, and the decision trail.
Resilience Primitives¶
Cross-cutting resilience is provided by concrete, programmatic components rather than attributes.
CircuitBreaker<TResult>¶
Whizbang.Core.Resilience.CircuitBreaker<TResult> wraps any async operation to prevent cascading failures during sustained outages. It is options-configured and returns a caller-supplied fallback value while the circuit is open:
Wrap an operation in a circuit breaker
using Whizbang.Core.Resilience;
var breaker = new CircuitBreaker<ServiceResult>(new CircuitBreakerOptions {
FailureThreshold = 5, // consecutive failures before opening (default 5)
InitialCooldownSeconds = 3, // first cooldown (default 3); doubles per re-open
CooldownBackoffMultiplier = 2.0, // 3s → 6s → 12s → 24s → ... (default 2.0)
MaxCooldownSeconds = 300, // backoff cap (default 300)
SuccessCacheDurationSeconds = 5 // cache successful results (default 5; 0 disables)
});
var result = await breaker.ExecuteAsync(
operation: ct => CallExternalServiceAsync(ct),
fallbackValue: ServiceResult.Unavailable,
cancellationToken: stoppingToken
);
Key behaviors (all verified against the implementation):
- State machine:
Closed → Open → HalfOpen → Closed(or back toOpenon a half-open failure). Current state is observable via theStateproperty (CircuitBreakerStateenum). - Escalating cooldown: each consecutive open doubles the cooldown (
InitialCooldownSeconds × CooldownBackoffMultiplier^n) up toMaxCooldownSeconds; the cooldown resets when the circuit closes. - Open-circuit fast-fail: while open and inside the cooldown,
ExecuteAsyncreturnsfallbackValuewithout invoking the operation. - Success caching: a successful result is cached for
SuccessCacheDurationSecondsand returned without re-executing the operation — set0to disable. - Observability:
ConsecutiveFailuresandCurrentCooldownSecondsare exposed for metrics/tests.
Circuit States¶
stateDiagram-v2
[*] --> Closed: Initial
Closed --> Open: FailureThreshold<br/>consecutive failures
Open --> HalfOpen: After<br/>cooldown
HalfOpen --> Closed: Success
HalfOpen --> Open: Failure<br/>(cooldown doubles)
Worker retry with exponential backoff¶
Message-processing retry is a worker concern, configured via WorkerRetryOptions rather than per-handler attributes:
WorkerRetryOptions defaults
public class WorkerRetryOptions {
public int RetryTimeoutSeconds { get; set; } = 1; // base timeout; first retry after 1s
public bool EnableExponentialBackoff { get; set; } = true;
public double BackoffMultiplier { get; set; } = 2.0; // 1s → 2s → 4s → 8s → 16s → 32s → 60s
public int MaxBackoffSeconds { get; set; } = 60; // cap — failing messages block their stream
}
The backoff cap is deliberately low: same-stream messages process in order (by UUIDv7), so a single failing message blocks all later messages in that stream until it completes or dead-letters.
Other resilience components¶
StreamRateLimiter(Whizbang.Core.Resilience) — per-stream rate limiting, configured viaStreamRateLimiterOptions.SubscriptionRetryHelper/SubscriptionResilienceOptions— retry/backoff for transport subscription establishment.ThrottleRetryOptions(Whizbang.Core.Workers) — throttle-aware retry tuning for transport publishing.
What Is Not Shipped¶
For clarity, the following do not exist in v1.0.0 — do not reference them in application code:
| Not shipped | Use instead |
|---|---|
[Retry] attribute on receptors |
WorkerRetryOptions (worker-level, applies to message processing) |
[Timeout] attribute |
CancellationToken-based timeouts in your handler; lease deadlines cancel hung dispatches automatically |
[Cache] attribute |
Standard IMemoryCache / your own caching in lenses |
[CircuitBreaker] attribute |
CircuitBreaker<TResult> (programmatic, options-based) |
IPolicyOf<T> / PolicyWeaver / [WhizbangPolicy] |
IPolicyEngine for routing decisions; resilience primitives above for fault handling |
Best Practices¶
- Route with policies, protect with primitives —
IPolicyEnginedecides where/how a message flows;CircuitBreaker<TResult>protects calls to fragile dependencies. - Don't retry non-transient errors — validation failures should dead-letter, not retry.
- Keep retry caps low — a failing message blocks its whole stream (ordered processing).
- Choose fallback values deliberately — an open circuit returns the fallback silently; make it distinguishable from a real result.
- Monitor breaker state —
State,ConsecutiveFailures, andCurrentCooldownSecondsare cheap to export as metrics.
Related Documentation¶
- Policy-Based Routing - Full guide to
IPolicyEngine,PolicyContext,PolicyConfiguration, and the decision trail - Receptors - Message handling components
- Dispatcher - How messages reach receptors
- Object Pooling -
PolicyContextpooling