├── .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 |
5 |
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 |
4 |
5 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------