|
| 1 | +# Resilience Patterns |
| 2 | + |
| 3 | +Building robust applications requires handling failures gracefully. RustAPI provides a suite of middleware to help your service survive partial outages, latency spikes, and transient errors. |
| 4 | + |
| 5 | +These patterns are essential for the "Enterprise Platform" learning path and microservices architectures. |
| 6 | + |
| 7 | +## Prerequisites |
| 8 | + |
| 9 | +Add the resilience features to your `Cargo.toml`. For example: |
| 10 | + |
| 11 | +```toml |
| 12 | +[dependencies] |
| 13 | +rustapi-rs = { version = "0.1.275", features = ["full"] } |
| 14 | +# OR cherry-pick features |
| 15 | +# rustapi-extras = { version = "0.1.275", features = ["circuit-breaker", "retry", "timeout"] } |
| 16 | +``` |
| 17 | + |
| 18 | +## Circuit Breaker |
| 19 | + |
| 20 | +The Circuit Breaker pattern prevents your application from repeatedly trying to execute an operation that's likely to fail. It gives the failing service time to recover. |
| 21 | + |
| 22 | +### How it works |
| 23 | +1. **Closed**: Requests flow normally. |
| 24 | +2. **Open**: After `failure_threshold` is reached, requests fail immediately with `503 Service Unavailable`. |
| 25 | +3. **Half-Open**: After `timeout` passes, a limited number of test requests are allowed. If they succeed, the circuit closes. |
| 26 | + |
| 27 | +### Usage |
| 28 | + |
| 29 | +```rust |
| 30 | +use rustapi_rs::prelude::*; |
| 31 | +use rustapi_extras::circuit_breaker::CircuitBreakerLayer; |
| 32 | +use std::time::Duration; |
| 33 | + |
| 34 | +fn main() { |
| 35 | + let app = RustApi::new() |
| 36 | + .layer( |
| 37 | + CircuitBreakerLayer::new() |
| 38 | + .failure_threshold(5) // Open after 5 failures |
| 39 | + .timeout(Duration::from_secs(30)) // Wait 30s before retrying |
| 40 | + .success_threshold(2) // Require 2 successes to close |
| 41 | + ) |
| 42 | + .route("/", get(handler)); |
| 43 | + |
| 44 | + // ... run app |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +## Retry with Backoff |
| 49 | + |
| 50 | +Transient failures (network blips, temporary timeouts) can often be resolved by simply retrying the request. The `RetryLayer` handles this automatically with configurable backoff strategies. |
| 51 | + |
| 52 | +### Strategies |
| 53 | +- **Exponential**: `base * 2^attempt` (Recommended for most cases) |
| 54 | +- **Linear**: `base * attempt` |
| 55 | +- **Fixed**: Constant delay |
| 56 | + |
| 57 | +### Usage |
| 58 | + |
| 59 | +```rust |
| 60 | +use rustapi_rs::prelude::*; |
| 61 | +use rustapi_extras::retry::{RetryLayer, RetryStrategy}; |
| 62 | +use std::time::Duration; |
| 63 | + |
| 64 | +fn main() { |
| 65 | + let app = RustApi::new() |
| 66 | + .layer( |
| 67 | + RetryLayer::new() |
| 68 | + .max_attempts(3) |
| 69 | + .initial_backoff(Duration::from_millis(100)) |
| 70 | + .max_backoff(Duration::from_secs(5)) |
| 71 | + .strategy(RetryStrategy::Exponential) |
| 72 | + .retryable_statuses(vec![500, 502, 503, 504, 429]) |
| 73 | + ) |
| 74 | + .route("/", get(handler)); |
| 75 | + |
| 76 | + // ... run app |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +> **Warning**: Be careful when combining Retries with non-idempotent operations (like `POST` requests that charge a credit card). The middleware safely handles cloning requests, but your business logic must support it. |
| 81 | +
|
| 82 | +## Timeouts |
| 83 | + |
| 84 | +Never let a request hang indefinitely. The `TimeoutLayer` enforces a hard limit on request duration, returning `408 Request Timeout` if exceeded. |
| 85 | + |
| 86 | +### Usage |
| 87 | + |
| 88 | +```rust |
| 89 | +use rustapi_rs::prelude::*; |
| 90 | +use rustapi_extras::timeout::TimeoutLayer; |
| 91 | +use std::time::Duration; |
| 92 | + |
| 93 | +fn main() { |
| 94 | + let app = RustApi::new() |
| 95 | + // Fail if handler takes longer than 5 seconds |
| 96 | + .layer(TimeoutLayer::from_secs(5)) |
| 97 | + .route("/", get(slow_handler)); |
| 98 | + |
| 99 | + // ... run app |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +## Combining Layers (The Resilience Stack) |
| 104 | + |
| 105 | +Order matters! Timeout should be the "outermost" constraint, followed by Circuit Breaker, then Retry. |
| 106 | + |
| 107 | +In RustAPI (Tower) middleware, layers wrap around each other. The order you call `.layer()` wraps the *previous* service. |
| 108 | + |
| 109 | +**Recommended Order:** |
| 110 | +1. **Retry** (Inner): Retries specific failures from the handler. |
| 111 | +2. **Circuit Breaker** (Middle): Stops retrying if the system is overloaded. |
| 112 | +3. **Timeout** (Outer): Enforces global time limit including all retries. |
| 113 | + |
| 114 | +```rust |
| 115 | +let app = RustApi::new() |
| 116 | + // 1. Retry (handles transient errors) |
| 117 | + .layer(RetryLayer::new()) |
| 118 | + // 2. Circuit Breaker (protects upstream) |
| 119 | + .layer(CircuitBreakerLayer::new()) |
| 120 | + // 3. Timeout (applies to the whole operation) |
| 121 | + .layer(TimeoutLayer::from_secs(10)) |
| 122 | + .route("/", get(handler)); |
| 123 | +``` |
0 commit comments