A flexible circuit breaker library for Python with multiple failure detection strategies and Redis support
Project description
c-breaker
A flexible circuit breaker library for Python with multiple failure detection strategies and Redis support for distributed applications.
Features
-
4 Failure Detection Strategies:
- Time-based: Count failures within a time window
- Sliding Window: Track failure rate in last N calls
- Combined: Both time-based and sliding window
- Custom: Implement your own detection logic
-
Distributed Support: Redis storage backend for horizontal scaling (sync + async)
-
Easy to Use: Simple decorator interface
-
Async Support: Full async/await support for modern Python applications
Installation
pip install c-breaker
# With Redis support
pip install c-breaker[redis]
Quick Start
Basic Usage with Decorator
from cbreaker import circuit_breaker
@circuit_breaker(name="my_api")
def call_external_api():
# Your code here
return requests.get("https://api.example.com/data")
Async Support
import aiohttp
from cbreaker import circuit_breaker
@circuit_breaker(name="async_api", failure_threshold=3)
async def call_async_api():
async with aiohttp.ClientSession() as session:
async with session.get("https://api.example.com") as response:
return await response.json()
Failure Detection Strategies
Time-Based Detection
Trips the circuit if N failures occur within a time window:
from cbreaker import circuit_breaker, DetectorType
@circuit_breaker(
name="time_based_example",
detector_type=DetectorType.TIME_BASED,
failure_threshold=5, # 5 failures
time_window_seconds=60, # within 60 seconds
recovery_timeout=30.0, # wait 30s before recovery attempt
)
def my_function():
pass
Sliding Window Detection
Trips based on failure rate in the last N calls:
from cbreaker import circuit_breaker, DetectorType
@circuit_breaker(
name="sliding_window_example",
detector_type=DetectorType.SLIDING_WINDOW,
window_size=10, # track last 10 calls
failure_rate_threshold=0.5, # trip at 50% failure rate
min_calls=5, # require at least 5 calls
)
def my_function():
pass
Combined Detection
Uses both strategies - trips if EITHER condition is met:
from cbreaker import circuit_breaker, DetectorType
@circuit_breaker(
name="combined_example",
detector_type=DetectorType.COMBINED,
# Time-based settings
failure_threshold=5,
time_window_seconds=60,
# Sliding window settings
window_size=10,
failure_rate_threshold=0.5,
min_calls=5,
# Set to True to require BOTH conditions
require_both=False,
)
def my_function():
pass
Custom Failure Detector
Create your own detection logic:
from cbreaker import BaseFailureDetector, circuit_breaker
from typing import Any
class MyCustomDetector(BaseFailureDetector):
def __init__(self, max_failures: int = 3):
self.max_failures = max_failures
self.failure_count = 0
def record_success(self, timestamp: float) -> None:
self.failure_count = max(0, self.failure_count - 1)
def record_failure(self, timestamp: float, exception: Exception | None = None) -> None:
self.failure_count += 1
def should_trip(self) -> bool:
return self.failure_count >= self.max_failures
def reset(self) -> None:
self.failure_count = 0
def get_state(self) -> dict[str, Any]:
return {"failure_count": self.failure_count}
def load_state(self, state: dict[str, Any]) -> None:
self.failure_count = state.get("failure_count", 0)
@circuit_breaker(name="custom", detector=MyCustomDetector(max_failures=3))
def my_function():
pass
Redis Storage for Distributed Applications
Share circuit breaker state across multiple application instances:
Synchronous Redis
import redis
from cbreaker import circuit_breaker, RedisStorage
# Create Redis client
redis_client = redis.Redis(host='localhost', port=6379, db=0)
# Create storage
storage = RedisStorage(sync_client=redis_client)
@circuit_breaker(
name="distributed_service",
storage=storage,
failure_threshold=5,
)
def call_service():
pass
Asynchronous Redis
import redis.asyncio as aioredis
from cbreaker import circuit_breaker, RedisStorage
# Create async Redis client
redis_client = aioredis.Redis(host='localhost', port=6379, db=0)
# Create storage with async client
storage = RedisStorage(async_client=redis_client)
@circuit_breaker(
name="async_distributed_service",
storage=storage,
)
async def call_service():
pass
Advanced Usage
Fallback Function
Execute a fallback when the circuit is open:
def fallback_response(*args, **kwargs):
return {"status": "service_unavailable", "cached": True}
@circuit_breaker(
name="with_fallback",
fallback=fallback_response,
)
def call_api():
return requests.get("https://api.example.com").json()
Excluded Exceptions
Don't count certain exceptions as failures:
@circuit_breaker(
name="with_exclusions",
excluded_exceptions=(ValueError, KeyError),
)
def my_function(value):
if not value:
raise ValueError("Empty value") # Won't count as failure
return external_call()
State Change Callbacks
React to circuit state changes:
from cbreaker import CircuitState
def on_state_change(old_state: CircuitState, new_state: CircuitState):
print(f"Circuit changed from {old_state} to {new_state}")
if new_state == CircuitState.OPEN:
send_alert("Circuit breaker tripped!")
@circuit_breaker(
name="with_callback",
on_state_change=on_state_change,
)
def my_function():
pass
Manual Circuit Breaker Control
from cbreaker import CircuitBreaker, TimeBasedFailureDetector
# Create circuit breaker manually
breaker = CircuitBreaker(
name="manual_breaker",
failure_detector=TimeBasedFailureDetector(
failure_threshold=5,
time_window_seconds=60
),
recovery_timeout=30.0,
)
# Use with call method
result = breaker.call(my_function, arg1, arg2)
# Check state
print(f"State: {breaker.state}")
print(f"Is open: {breaker.is_open}")
# Get stats
stats = breaker.get_stats()
# Manual control
breaker.trip() # Force open
breaker.reset() # Force close
Access Circuit Breaker from Decorated Function
@circuit_breaker(name="my_service")
def my_function():
pass
# Access the circuit breaker instance
cb = my_function.circuit_breaker
print(f"Current state: {cb.state}")
print(f"Stats: {cb.get_stats()}")
Global Registry
Access all circuit breakers:
from cbreaker.decorators.circuit import get_circuit_breaker, get_all_circuit_breakers
# Get a specific circuit breaker
cb = get_circuit_breaker("my_service")
# Get all circuit breakers
all_breakers = get_all_circuit_breakers()
for name, breaker in all_breakers.items():
print(f"{name}: {breaker.state}")
Circuit Breaker States
| State | Description |
|---|---|
| CLOSED | Normal operation. Requests pass through. Failures are recorded. |
| OPEN | Circuit tripped. Requests are rejected with CircuitOpenError. |
| HALF_OPEN | Testing recovery. Limited requests allowed to test if service recovered. |
Configuration Options
| Option | Default | Description |
|---|---|---|
name |
Function name | Unique identifier for the circuit breaker |
detector_type |
DetectorType.TIME_BASED |
Detection strategy: DetectorType.TIME_BASED, DetectorType.SLIDING_WINDOW, DetectorType.COMBINED (strings also accepted) |
failure_threshold |
5 |
Failures to trip (time-based) |
time_window_seconds |
60.0 |
Time window in seconds (time-based) |
window_size |
10 |
Calls to track (sliding window) |
failure_rate_threshold |
0.5 |
Failure rate to trip (sliding window) |
min_calls |
5 |
Min calls before tripping (sliding window) |
recovery_timeout |
30.0 |
Seconds before OPEN → HALF_OPEN |
half_open_max_calls |
1 |
Calls allowed in HALF_OPEN |
excluded_exceptions |
None |
Exceptions that don't count as failures |
storage |
MemoryStorage |
State storage backend |
fallback |
None |
Function to call when circuit is open |
on_state_change |
None |
Callback for state changes |
License
MIT License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file c_breaker-0.1.6.tar.gz.
File metadata
- Download URL: c_breaker-0.1.6.tar.gz
- Upload date:
- Size: 50.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
091d832d6396043b48cbcccf007417efeea17aff8c1ee18d676d2ada7c040fe9
|
|
| MD5 |
1a2fe5feeaeab495b7a3bbc03c0c8b43
|
|
| BLAKE2b-256 |
4604362350f78c136c03da3ba3a269ca8b967fc512d82648460a2ec96b16c610
|
Provenance
The following attestation bundles were made for c_breaker-0.1.6.tar.gz:
Publisher:
publish.yml on 500ping/c-breaker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
c_breaker-0.1.6.tar.gz -
Subject digest:
091d832d6396043b48cbcccf007417efeea17aff8c1ee18d676d2ada7c040fe9 - Sigstore transparency entry: 804374604
- Sigstore integration time:
-
Permalink:
500ping/c-breaker@59cd4d13d7e8dca4dd6a95c3f61a00a70e231c22 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/500ping
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@59cd4d13d7e8dca4dd6a95c3f61a00a70e231c22 -
Trigger Event:
push
-
Statement type:
File details
Details for the file c_breaker-0.1.6-py3-none-any.whl.
File metadata
- Download URL: c_breaker-0.1.6-py3-none-any.whl
- Upload date:
- Size: 23.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c81267bc2cb45708d368b0814070b6557d838220e7edbd5dfe0d2b692e9e4723
|
|
| MD5 |
0b0e10e2e21141544698e1f113361fb9
|
|
| BLAKE2b-256 |
2bee380f865cc0dcba9ce1200663cdae6980dce61330ad6240abd8a1f100f415
|
Provenance
The following attestation bundles were made for c_breaker-0.1.6-py3-none-any.whl:
Publisher:
publish.yml on 500ping/c-breaker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
c_breaker-0.1.6-py3-none-any.whl -
Subject digest:
c81267bc2cb45708d368b0814070b6557d838220e7edbd5dfe0d2b692e9e4723 - Sigstore transparency entry: 804374605
- Sigstore integration time:
-
Permalink:
500ping/c-breaker@59cd4d13d7e8dca4dd6a95c3f61a00a70e231c22 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/500ping
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@59cd4d13d7e8dca4dd6a95c3f61a00a70e231c22 -
Trigger Event:
push
-
Statement type: