pub struct RedisCircuitBreaker { /* private fields */ }Expand description
Per-backend circuit breaker, persisted in Redis.
Same Closed → Open → HalfOpen state machine as CircuitBreaker, and the
same method names, but every operation is a synchronous Redis round trip
(via the shared [crate::redis_protocol] RESP client) instead of an
in-memory HashMap update — so state survives a process restart, and is
shared across every rws instance pointed at the same Redis server.
§Why Redis, not the model layer
The model layer (DbPool, sqlx) is async fn-only, while
CircuitBreaker’s methods and Middleware::handle (what RetryLayer
implements) are both synchronous — the identical async/sync mismatch that
left SqliteRateLimiter unbuilt after crate::rate_limit::RedisRateLimiter
shipped. Redis, reached over a plain blocking TcpStream, stays fully
synchronous and drops into the same call sites CircuitBreaker already
has, with no new Cargo dependency.
§Consistency
Each operation is a read-then-write (GET then SET) against one Redis
key per backend — not a single atomic command. Two rws instances racing
to record a failure for the same backend at the same instant can lose one
of the two increments. This is a deliberate simplification: unlike a rate
limit (a hard resource/security boundary, where RedisRateLimiter uses
genuinely atomic INCR for exactly this reason), a circuit breaker is a
self-healing heuristic where opening one failure late — or one request
later than a perfectly-synchronized count would — has no real consequence.
§Example
use rust_web_server::circuit_breaker::RedisCircuitBreaker;
let breaker = RedisCircuitBreaker::new("127.0.0.1:6379", None, 5, 30);
match breaker.is_available("backend-a:8080") {
Ok(true) => { /* forward the request */ }
Ok(false) => { /* short-circuit — return 503 without contacting the backend */ }
Err(e) => { /* Redis unreachable — decide fail-open vs fail-closed yourself */ }
}Implementations§
Source§impl RedisCircuitBreaker
impl RedisCircuitBreaker
Sourcepub fn new(
addr: impl Into<String>,
password: Option<String>,
failure_threshold: u32,
recovery_secs: u64,
) -> Self
pub fn new( addr: impl Into<String>, password: Option<String>, failure_threshold: u32, recovery_secs: u64, ) -> Self
Create a breaker that connects to addr (e.g. "127.0.0.1:6379").
password is passed to Redis AUTH if Some.
Sourcepub fn from_env() -> Self
pub fn from_env() -> Self
Build a breaker from environment variables:
RWS_REDIS_HOST(default127.0.0.1)RWS_REDIS_PORT(default6379)RWS_REDIS_PASSWORD(optional)RWS_CONFIG_CIRCUIT_BREAKER_FAILURE_THRESHOLD(default5)RWS_CONFIG_CIRCUIT_BREAKER_RECOVERY_SECS(default30)
Sourcepub fn set_limits(&self, failure_threshold: u32, recovery_secs: u64)
pub fn set_limits(&self, failure_threshold: u32, recovery_secs: u64)
Update the thresholds on a live breaker without restarting.
Sourcepub fn is_available(&self, backend: &str) -> Result<bool>
pub fn is_available(&self, backend: &str) -> Result<bool>
Returns Ok(true) if a request should be forwarded to backend.
Transitions Open → HalfOpen when the recovery window has elapsed.
Returns Err if Redis is unreachable — callers decide whether that
means fail open (treat as available) or fail closed (treat as not).
Sourcepub fn record_success(&self, backend: &str) -> Result<()>
pub fn record_success(&self, backend: &str) -> Result<()>
Record a successful response for backend.
Transitions HalfOpen → Closed and resets the failure counter.
Sourcepub fn record_failure(&self, backend: &str) -> Result<()>
pub fn record_failure(&self, backend: &str) -> Result<()>
Record a failed response for backend.
In Closed state, increments the counter and opens the circuit when
failure_threshold is reached. In HalfOpen state, immediately
re-opens the circuit and resets the recovery timer.
Sourcepub fn state(&self, backend: &str) -> Result<BreakerState>
pub fn state(&self, backend: &str) -> Result<BreakerState>
Return the current state for backend (defaults to Closed if unseen).