feat(circuit-breaker): add CircuitBreaker Tower middleware#855
feat(circuit-breaker): add CircuitBreaker Tower middleware#855Mattbusel wants to merge 3 commits intotower-rs:masterfrom
Conversation
Three-state machine (Closed → Open → HalfOpen) with configurable failure threshold, success-rate recovery, and probe timeout. - CircuitBreakerLayer for ServiceBuilder ergonomics - CircuitBreaker<S> implements Service<Request> - ResponseFuture: non-blocking gate check via try_read() - Automatic HalfOpen transition after timeout elapses - Clears result window on HalfOpen so recovery rate reflects only post-recovery probes, not stale failure history - Full test coverage for open/close/recovery paths Designed and implemented by Matthew Busel.
…Debug - Replace tokio::sync::RwLock with std::sync::Mutex — state updates now happen synchronously in poll() and poll_ready(), eliminating the tokio::spawn-inside-poll anti-pattern - Circuit gate check moved to poll_ready() where Tower expects it; call() only wraps the inner future in ResponseFuture - ResponseFuture::poll updates state inline on Ready — no allocation or task spawn, correct under Tower's single-threaded test executor - Suppress missing_debug_implementations for CircuitBreaker<S> since S is an unconstrained generic (same pattern as tower::Timeout<S>) - cargo fmt applied Designed and implemented by Matthew Busel.
|
Thanks for restarting this! Just a couple thoughts as I look through it:
|
|
Good points, thanks. On Budget, I see them as complementary layers: Budget governs retry-worthiness, circuit breaker gates all traffic (including first attempts) when the backend is down. They compose rather than overlap. Happy to add a note in the docs making that distinction explicit. On the broader abstraction, agreed. I'll refactor to a Will push an updated draft. |
…ionship - Extract CircuitPolicy trait (on_success, on_failure, should_probe, on_half_open) - Move ConsecutiveFailures into policy.rs as the built-in implementation - CircuitBreaker<S, P> generic over CircuitPolicy; SharedState<P> replaces State - CircuitBreakerLayer<P> with ::new() and ::with_policy() constructors - ResponseFuture<F, T, E, P> delegates outcome reporting to the policy - Document Send/Sync expectations on CircuitPolicy and CircuitBreaker structs - Document budget vs circuit breaker relationship in mod.rs and policy.rs - Add custom_policy_is_accepted test
|
Pushed ddb88ba, CircuitPolicy trait extracted, budget relationship documented. Ready for another look. |
Problem
Tower has no built-in circuit breaker. PR #102 was closed during migration in 2019 with a note to pick it back up — it never was. Users building on
reqwest,hyper, ortonicare forced to either write their own or pull in a separate crate just for this pattern.The missing primitive means retry storms: when a backend goes down, requests pile up, timeouts accumulate, and memory/goroutine equivalents grow unbounded. A circuit breaker cuts this off at the source.
Solution
This PR adds
tower::circuit_breaker— a three-state machine implemented as a standard TowerService<Request>+Layer.States
Usage
Key design decisions
try_read()inpoll()— circuit gate check is non-blocking; wakes and yields rather than blocking the executor if the write lock is held during a state transition.CircuitError<E>— wraps the inner error type;CircuitError::Opensignals a rejected-without-calling case so callers can distinguish "backend failed" from "circuit open".reset()method — allows operator-driven forced close (e.g. after confirming backend is healthy).circuit-breaker = ["tokio/sync", "tokio/time", "pin-project-lite"]— zero cost if unused.Files changed
tower/src/circuit_breaker/mod.rstower/src/circuit_breaker/layer.rsCircuitBreakerLayertower/src/circuit_breaker/service.rsCircuitBreaker<S>+ state machine + teststower/src/circuit_breaker/future.rsResponseFuturewith non-blocking gatetower/src/lib.rs#[cfg(feature = "circuit-breaker")] pub mod circuit_breakertower/Cargo.tomlfullTests
Two inline tests in
service.rs:closed_passes_requests_through— baseline happy pathopens_after_failure_threshold— verifies Open state rejects withCircuitError::OpenDesigned and implemented by Matthew Busel.