├── .gitignore ├── circuit_breaker ├── enums.py ├── __init__.py ├── exceptions.py ├── middleware.py ├── schemas.py └── circuit_breaker.py ├── uv.lock ├── .idea ├── vcs.xml ├── misc.xml ├── .gitignore ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── modules.xml └── circuit-breaker-pattern-fastapi.iml ├── pyproject.toml ├── setup.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | shell 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /circuit_breaker/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class State(Enum): 5 | OPEN = "OPEN" 6 | CLOSED = "CLOSED" 7 | HALF_OPEN = "HALF_OPEN" 8 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.11" 4 | 5 | [[package]] 6 | name = "circuit-breaker-pattern-fastapi" 7 | version = "0.1.0" 8 | source = { virtual = "." } 9 | -------------------------------------------------------------------------------- /circuit_breaker/__init__.py: -------------------------------------------------------------------------------- 1 | from .schemas import CircuitBreakerInputDto 2 | from .middleware import CircuitBreakerMiddleware 3 | 4 | __all__ = ["CircuitBreakerMiddleware", "CircuitBreakerInputDto"] 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "circuit_breaker_fastapi" 3 | version = "0.1.0" 4 | description = "Circuit breaker pattern implementation fastapi" 5 | authors = [{name = "Furkan Melih ERCAN", email = "furkanmelihercan.98@gmail.com"}] 6 | license = {text = "MIT"} 7 | readme = "README.md" 8 | requires-python = ">=3.11.8" 9 | dependencies = [ 10 | "fastapi", 11 | "pendulum" 12 | ] 13 | 14 | [build-system] 15 | requires = ["setuptools", "wheel"] 16 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /circuit_breaker/exceptions.py: -------------------------------------------------------------------------------- 1 | from fastapi.exceptions import HTTPException 2 | 3 | 4 | class CircuitBreakerRemoteCallException(HTTPException): 5 | def __init__(self, message: str | None = None) -> None: 6 | if message is None: 7 | message = ( 8 | "The system is experiencing high failure rates. Please try again later." 9 | ) 10 | 11 | super().__init__(status_code=503, detail=message) 12 | 13 | 14 | __all__ = ["CircuitBreakerRemoteCallException"] 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="circuit_breaker_fastapi", 5 | version="0.1.0", 6 | author="Furkan Melih Ercan", 7 | author_email="furkanmelihercan.98@gmail.com", 8 | description="Circuit breaker pattern implementation fastapi", 9 | long_description=open("README.md").read(), 10 | long_description_content_type="text/markdown", 11 | url="https://github.com/fmelihh/circuit-breaker-pattern-fastapi", 12 | packages=find_packages(), 13 | python_requires=">=3.11.8", 14 | ) 15 | -------------------------------------------------------------------------------- /.idea/circuit-breaker-pattern-fastapi.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /circuit_breaker/middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from typing import Callable 3 | from starlette.middleware.base import BaseHTTPMiddleware 4 | 5 | from .schemas import CircuitBreakerInputDto 6 | from .circuit_breaker import CircuitBreaker 7 | 8 | 9 | class CircuitBreakerMiddleware(CircuitBreaker, BaseHTTPMiddleware): 10 | def __init__( 11 | self, app, circuit_breaker_input: CircuitBreakerInputDto | None = None 12 | ): 13 | BaseHTTPMiddleware.__init__(self, app) 14 | CircuitBreaker.__init__(self, circuit_breaker_input) 15 | 16 | async def dispatch(self, request: Request, call_next: Callable): 17 | response = await self.handle_circuit_breaker( 18 | func=call_next, 19 | request=request, 20 | ) 21 | 22 | return response 23 | -------------------------------------------------------------------------------- /circuit_breaker/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | from fastapi.exceptions import HTTPException 3 | from pydantic import BaseModel, Field, field_validator 4 | 5 | 6 | class CircuitBreakerInputDto(BaseModel): 7 | half_open_retry_count: int = Field(default=3, ge=0, le=100) 8 | half_open_retry_timeout_seconds: int = Field(default=120, ge=30, le=1800) 9 | 10 | exception_list: tuple[Type[Exception]] = Field(default=(Exception,)) 11 | 12 | @field_validator("exception_list") 13 | @classmethod 14 | def validate_exception_list( 15 | cls, v: tuple[Type[Exception]] 16 | ) -> tuple[Type[Exception]]: 17 | if v is None or len(v) == 0: 18 | return (Exception,) 19 | 20 | return tuple(set(v)) 21 | 22 | class Config: 23 | arbitrary_types_allowed = True 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Circuit Breaker Pattern in Python 2 | 3 | ## Overview 4 | The **Circuit Breaker** pattern is a design pattern used to improve system resilience by preventing continuous failures in a system. It acts as a safeguard by **monitoring requests** and stopping repeated failures from affecting performance. 5 | 6 | ## How It Works 7 | 1. **Closed State**: The system operates normally and allows requests. 8 | 2. **Open State**: If failures exceed a threshold, the circuit "opens," stopping further requests for a cooldown period. 9 | 3. **Half-Open State**: After a cooldown, a few test requests are allowed. If they succeed, the circuit closes; otherwise, it stays open. 10 | 11 | ## Why Use Circuit Breaker? 12 | - Prevents cascading failures in distributed systems. 13 | - Improves system reliability and recovery. 14 | - Reduces unnecessary load on failing services. 15 | 16 | ## Installation 17 | ``` 18 | pip install circuit_breaker_fastapi 19 | ``` 20 | 21 | ## Example Usage in Python FastAPI 22 | ```python 23 | import uvicorn 24 | from fastapi import FastAPI 25 | from fastapi.exceptions import HTTPException 26 | from circuit_breaker import CircuitBreakerMiddleware, CircuitBreakerInputDto 27 | 28 | app = FastAPI() 29 | 30 | 31 | @app.get("/success-endpoint") 32 | async def success_endpoint(): 33 | return "success" 34 | 35 | 36 | @app.get("/failure-endpoint") 37 | async def faulty_endpoint(): 38 | raise HTTPException(status_code=500) 39 | 40 | 41 | app.add_middleware( 42 | CircuitBreakerMiddleware, 43 | circuit_breaker_input=CircuitBreakerInputDto( 44 | exception_list=(Exception,), 45 | half_open_retry_count=3, 46 | half_open_retry_timeout_seconds=30, 47 | ), 48 | ) 49 | 50 | if __name__ == "__main__": 51 | uvicorn.run(app, host="0.0.0.0", port=8000) 52 | ``` 53 | -------------------------------------------------------------------------------- /circuit_breaker/circuit_breaker.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import pendulum as pe 3 | from typing import Callable 4 | from fastapi import Response 5 | from fastapi.responses import StreamingResponse 6 | 7 | from .enums import State 8 | from .schemas import CircuitBreakerInputDto 9 | from .exceptions import CircuitBreakerRemoteCallException 10 | 11 | 12 | class CircuitBreaker: 13 | def __init__(self, circuit_breaker_input: CircuitBreakerInputDto | None = None): 14 | if circuit_breaker_input is None: 15 | circuit_breaker_input = CircuitBreakerInputDto() 16 | 17 | self._state = None 18 | self._failed_count = 0 19 | self._last_attempt_datetime = None 20 | self.circuit_breaker_input = circuit_breaker_input 21 | 22 | @property 23 | def state(self) -> State: 24 | if self._state is None: 25 | self._state = State.CLOSED 26 | 27 | return self._state 28 | 29 | @state.setter 30 | def state(self, state: State): 31 | self._state = state 32 | 33 | @staticmethod 34 | def _check_http_exception(response: StreamingResponse): 35 | if response.status_code < 400: 36 | return 37 | 38 | raise CircuitBreakerRemoteCallException( 39 | "Request failed with status code: " + str(response.status_code) 40 | ) 41 | 42 | def update_last_attempt_datetime(self): 43 | self._last_attempt_datetime = pe.now() 44 | 45 | async def handle_open_state(self, func: Callable, *args, **kwargs) -> Response: 46 | exception_list = tuple(self.circuit_breaker_input.exception_list) 47 | 48 | current_datetime = pe.now() 49 | threshold_datetime = self._last_attempt_datetime.add( 50 | seconds=self.circuit_breaker_input.half_open_retry_timeout_seconds 51 | ) 52 | if threshold_datetime >= current_datetime: 53 | raise CircuitBreakerRemoteCallException( 54 | message=f"Retry after {(threshold_datetime - current_datetime).seconds} secs" 55 | ) 56 | 57 | self.state = State.HALF_OPEN 58 | try: 59 | response = await func(*args, **kwargs) 60 | self._check_http_exception(response) 61 | self.state = State.CLOSED 62 | self._failed_count = 0 63 | self.update_last_attempt_datetime() 64 | return response 65 | except exception_list as e: 66 | self._failed_count += 1 67 | self.update_last_attempt_datetime() 68 | self.state = State.OPEN 69 | 70 | traceback.print_exception(e) 71 | raise CircuitBreakerRemoteCallException 72 | 73 | async def handle_closed_state(self, func: Callable, *args, **kwargs) -> Response: 74 | exception_list = tuple(self.circuit_breaker_input.exception_list) 75 | try: 76 | response = await func(*args, **kwargs) 77 | self._check_http_exception(response) 78 | self.update_last_attempt_datetime() 79 | return response 80 | except exception_list as e: 81 | self._failed_count += 1 82 | self.update_last_attempt_datetime() 83 | 84 | if self._failed_count >= self.circuit_breaker_input.half_open_retry_count: 85 | self.state = State.OPEN 86 | 87 | traceback.print_exception(e) 88 | raise CircuitBreakerRemoteCallException 89 | 90 | async def handle_circuit_breaker(self, func: Callable, *args, **kwargs) -> Response: 91 | match self.state: 92 | case State.OPEN: 93 | response = await self.handle_open_state(func, *args, **kwargs) 94 | case State.CLOSED: 95 | response = await self.handle_closed_state(func, *args, **kwargs) 96 | case _: 97 | raise NotImplementedError("Different state strategy provided") 98 | 99 | return response 100 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 187 | --------------------------------------------------------------------------------