Circuit Breaker¶
What is a circuit breaker?¶
A circuit breaker prevents cascading failures by stopping requests to a failing service. When failures exceed a threshold, the circuit "opens" and all requests are immediately denied. After a recovery timeout, it enters "half-open" state and allows one probe request. If the probe succeeds, the circuit closes; if it fails, it opens again.
Why it matters¶
Without a circuit breaker, a failing service causes: - Request timeouts piling up - Resource exhaustion - Cascading failures across your system
The circuit breaker fails fast, preserving resources and giving the failing service time to recover.
States¶
CLOSED (normal) --[failures >= threshold]--> OPEN (blocking)
^ |
| [timeout]
| v
+--[probe success]-- HALF-OPEN --[probe failure]--> OPEN
| State | Behavior |
|---|---|
| CLOSED | Normal operation, requests pass through |
| OPEN | All requests denied immediately |
| HALF-OPEN | Allows one probe request to test recovery |
Usage¶
from drogue.protection.circuit import CircuitBreaker
breaker = CircuitBreaker(
failure_threshold=5, # Failures before opening
recovery_timeout=30.0, # Seconds before half-open
jitter=0.2, # Random jitter on timeout
half_open_max_calls=1, # Probes allowed in half-open
)
# Check if request is allowed
if breaker.allow_request():
try:
result = call_external_service()
breaker.record_success()
except Exception:
breaker.record_failure()
else:
# Circuit is open, fail fast
return {"error": "Service unavailable", "retry_after": 30}
Response examples¶
get_status() response¶
status = breaker.get_status()
# {
# "state": "closed", # Current state
# "failure_count": 0, # Consecutive failures
# "success_count": 42, # Consecutive successes
# "last_failure_time": None, # Timestamp of last failure
# "half_open_calls": 0, # Probes in half-open
# }
state.value response¶
breaker.state.value
# "closed" -- normal operation
# "open" -- blocking all requests
# "half_open" -- testing recovery
State transitions¶
1. Start in CLOSED state
2. Each failure increments failure_count
3. When failure_count >= failure_threshold:
- State changes to OPEN
- All requests denied
4. After recovery_timeout seconds:
- State changes to HALF-OPEN
- One probe request is allowed
5. If probe succeeds:
- State changes to CLOSED
- failure_count resets to 0
6. If probe fails:
- State changes back to OPEN
- recovery_timeout restarts
Manual operations¶
# Force reset to closed
breaker.reset()
# Check current state
if breaker.state.value == "open":
print("Circuit is open, failing fast")
Configuration¶
from drogue.core.config import DrogueConfig
config = DrogueConfig(
circuit_breaker_enabled=True,
circuit_failure_threshold=5,
circuit_recovery_timeout=30.0,
circuit_jitter=0.2,
)
Jitter behavior (fixed in v0.2.1): The jitter is now sampled once when the circuit opens, and stored as a fixed recovery deadline. This prevents the thundering herd problem that occurred when the jitter was re-rolled on every allow_request() call — polling the circuit would have effectively eroded the timeout, causing all clients to recover nearly simultaneously.
Thread safety (added in v0.2.1): All state transitions are now protected by an internal lock, making CircuitBreaker safe for concurrent use across threads/tasks.
Example: External API protection¶
from drogue.protection.circuit import CircuitBreaker
import httpx
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=60.0)
async def call_payment_api(data):
if not breaker.allow_request():
raise Exception("Payment API unavailable, try again later")
try:
async with httpx.AsyncClient() as client:
response = await client.post("https://api.payments.com/charge", json=data)
response.raise_for_status()
breaker.record_success()
return response.json()
except Exception as e:
breaker.record_failure()
raise