Stop calling failing services. Automatic recovery.
import asyncio
import httpx
from enum import Enum
from dataclasses import dataclass
import time
class CircuitState(Enum):
CLOSED = "closed" # Normal operation
OPEN = "open" # Failing, reject requests
HALF_OPEN = "half_open" # Testing if recovered
@dataclass
class CircuitBreaker:
failure_threshold: int = 5 # failures before opening
recovery_timeout: int = 30 # seconds before half-open
def __init__(self):
self.state = CircuitState.CLOSED
self.failure_count = 0
self.last_failure_time = None
def is_available(self) -> bool:
"""Check if circuit allows requests."""
if self.state == CircuitState.CLOSED:
return True
if self.state == CircuitState.OPEN:
# Check if timeout elapsed
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
return True # Allow test request
return False
if self.state == CircuitState.HALF_OPEN:
return True # Allow test request
def record_success(self):
"""Record successful call."""
self.failure_count = 0
if self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.CLOSED
def record_failure(self):
"""Record failed call."""
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
async def call_with_circuit_breaker(cb: CircuitBreaker, client: httpx.AsyncClient, url: str):
if not cb.is_available():
raise Exception("Circuit breaker is OPEN, service unavailable")
try:
response = await client.get(url, timeout=5)
response.raise_for_status()
cb.record_success()
return response.json()
except Exception as e:
cb.record_failure()
raise
async def main():
cb = CircuitBreaker(failure_threshold=3, recovery_timeout=10)
async with httpx.AsyncClient() as client:
for i in range(100):
try:
result = await call_with_circuit_breaker(cb, client, "https://api.example.com/data")
print(f"Success: {result}")
except Exception as e:
print(f"Failed: {e} (circuit state: {cb.state.value})")
await asyncio.sleep(1)
asyncio.run(main())
async def call_with_fallback(cb, client, url, fallback_fn):
if not cb.is_available():
# Circuit is OPEN, use fallback (cache, default value)
return await fallback_fn()
try:
response = await client.get(url)
cb.record_success()
return response.json()
except Exception as e:
cb.record_failure()
return await fallback_fn() # Fallback on failure
# Usage in FastAPI
@app.get("/user/{user_id}")
async def get_user(user_id: int):
return await call_with_fallback(
cb,
client,
f"https://user-service.example.com/users/{user_id}",
fallback_fn=lambda: {"id": user_id, "cached": True} # Return cached/default
)