Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions circuit_breaker_box/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@
from circuit_breaker_box.circuit_breaker_redis import CircuitBreakerRedis
from circuit_breaker_box.common_types import ResponseType
from circuit_breaker_box.errors import BaseCircuitBreakerError, HostUnavailableError
from circuit_breaker_box.retryer_base import BaseRetrier
from circuit_breaker_box.retryers import Retrier, RetrierCircuitBreaker
from circuit_breaker_box.retryer import Retrier


__all__ = [
"BaseCircuitBreaker",
"BaseCircuitBreakerError",
"BaseRetrier",
"CircuitBreakerInMemory",
"CircuitBreakerRedis",
"HostUnavailableError",
"ResponseType",
"Retrier",
"RetrierCircuitBreaker",
]
35 changes: 35 additions & 0 deletions circuit_breaker_box/common_types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,39 @@
import typing

import tenacity


ResponseType = typing.TypeVar("ResponseType")

stop_types = (
tenacity.stop.stop_after_attempt
| tenacity.stop.stop_all
| tenacity.stop.stop_any
| tenacity.stop.stop_when_event_set
| tenacity.stop.stop_after_delay
| tenacity.stop.stop_before_delay
)
wait_types = (
tenacity.wait.wait_fixed
| tenacity.wait.wait_none
| tenacity.wait.wait_random
| tenacity.wait.wait_combine
| tenacity.wait.wait_chain
| tenacity.wait.wait_incrementing
| tenacity.wait.wait_exponential
| tenacity.wait.wait_random_exponential
| tenacity.wait.wait_exponential_jitter
)
retry_clause_types = (
tenacity.retry_if_exception
| tenacity.retry_if_exception_type
| tenacity.retry_if_not_exception_type
| tenacity.retry_unless_exception_type
| tenacity.retry_if_exception_cause_type
| tenacity.retry_if_result
| tenacity.retry_if_not_result
| tenacity.retry_if_exception_message
| tenacity.retry_if_not_exception_message
| tenacity.retry_any
| tenacity.retry_all
)
60 changes: 60 additions & 0 deletions circuit_breaker_box/retryer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import abc
import dataclasses
import logging
import typing

import tenacity

from circuit_breaker_box import BaseCircuitBreaker, ResponseType
from circuit_breaker_box.common_types import retry_clause_types, stop_types, wait_types


logger = logging.getLogger(__name__)

P = typing.ParamSpec("P")


@dataclasses.dataclass(kw_only=True)
class Retrier(abc.ABC, typing.Generic[ResponseType]):
reraise: bool = True
wait_strategy: wait_types
stop_rule: stop_types
retry_cause: retry_clause_types
circuit_breaker: BaseCircuitBreaker | None = None

async def retry( # type: ignore[return]
self,
coroutine: typing.Callable[P, typing.Awaitable[ResponseType]],
/,
host: str | None = None,
*args: P.args,
**kwargs: P.kwargs,
) -> ResponseType:
if not host and self.circuit_breaker:
msg = "'host' argument should be defined"
raise ValueError(msg)

for attempt in tenacity.Retrying( # noqa: RET503
stop=self.stop_rule,
wait=self.wait_strategy,
retry=self.retry_cause,
reraise=self.reraise,
before=self._log_attempts,
):
with attempt:
if self.circuit_breaker and host:
if not await self.circuit_breaker.is_host_available(host):
await self.circuit_breaker.raise_host_unavailable_error(host)

if attempt.retry_state.attempt_number > 1:
await self.circuit_breaker.increment_failures_count(host)

return await coroutine(*args, **kwargs)

@staticmethod
def _log_attempts(retry_state: tenacity.RetryCallState) -> None:
logger.info(
"Attempt: attempt_number: %s, outcome_timestamp: %s",
retry_state.attempt_number,
retry_state.outcome_timestamp,
)
36 changes: 0 additions & 36 deletions circuit_breaker_box/retryer_base.py

This file was deleted.

65 changes: 0 additions & 65 deletions circuit_breaker_box/retryers.py

This file was deleted.

13 changes: 6 additions & 7 deletions examples/example_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@
import logging

import httpx
import tenacity

from circuit_breaker_box.retryers import Retrier
from circuit_breaker_box.retryer import Retrier


MAX_RETRIES = 4
MAX_CACHE_SIZE = 256
CIRCUIT_BREAKER_MAX_FAILURE_COUNT = 3
RESET_TIMEOUT_IN_SECONDS = 10
SOME_HOST = "http://example.com/"


async def main() -> None:
logging.basicConfig(level=logging.DEBUG)
retryer = Retrier[httpx.Response](
max_retries=MAX_RETRIES,
exceptions_to_retry=(ZeroDivisionError,),
stop_rule=tenacity.stop.stop_after_attempt(MAX_RETRIES),
retry_cause=tenacity.retry_if_exception_type(ZeroDivisionError),
wait_strategy=tenacity.wait_none(),
)
example_request = httpx.Request("GET", httpx.URL("http://example.com"))
example_request = httpx.Request("GET", httpx.URL(SOME_HOST))

async def foo(request: httpx.Request) -> httpx.Response: # noqa: ARG001
raise ZeroDivisionError
Expand Down
10 changes: 6 additions & 4 deletions examples/example_retry_circuit_breaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import fastapi
import httpx
import tenacity

from circuit_breaker_box import CircuitBreakerInMemory, RetrierCircuitBreaker
from circuit_breaker_box import CircuitBreakerInMemory, Retrier


MAX_RETRIES = 4
Expand All @@ -27,10 +28,11 @@ async def main() -> None:
max_failure_count=CIRCUIT_BREAKER_MAX_FAILURE_COUNT,
max_cache_size=MAX_CACHE_SIZE,
)
retryer = RetrierCircuitBreaker[httpx.Response](
retryer = Retrier[httpx.Response](
circuit_breaker=circuit_breaker,
max_retries=MAX_RETRIES,
exceptions_to_retry=(ZeroDivisionError, httpx.RequestError),
wait_strategy=tenacity.wait_exponential_jitter(),
retry_cause=tenacity.retry_if_exception_type((ZeroDivisionError, httpx.RequestError)),
stop_rule=tenacity.stop.stop_after_attempt(MAX_RETRIES),
)
example_request = httpx.Request("GET", httpx.URL("http://example.com"))

Expand Down
17 changes: 10 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

import httpx
import pytest
import tenacity
from redis import asyncio as aioredis

from circuit_breaker_box import CircuitBreakerInMemory, Retrier, RetrierCircuitBreaker
from circuit_breaker_box import CircuitBreakerInMemory, Retrier
from circuit_breaker_box.circuit_breaker_redis import CircuitBreakerRedis
from examples.example_retry_circuit_breaker import CustomCircuitBreakerInMemory

Expand Down Expand Up @@ -68,17 +69,19 @@ def fixture_custom_circuit_breaker_in_memory() -> CustomCircuitBreakerInMemory:
@pytest.fixture(name="test_retry_without_circuit_breaker")
def fixture_retry_without_circuit_breaker() -> Retrier[httpx.Response]:
return Retrier[httpx.Response](
max_retries=MAX_RETRIES,
exceptions_to_retry=(ZeroDivisionError,),
stop_rule=tenacity.stop.stop_after_attempt(MAX_RETRIES),
retry_cause=tenacity.retry_if_exception_type(ZeroDivisionError),
wait_strategy=tenacity.wait_none(),
)


@pytest.fixture(name="test_retry_custom_circuit_breaker_in_memory")
def fixture_retry_custom_circuit_breaker_in_memory(
test_custom_circuit_breaker_in_memory: CustomCircuitBreakerInMemory,
) -> RetrierCircuitBreaker[httpx.Response]:
return RetrierCircuitBreaker[httpx.Response](
) -> Retrier[httpx.Response]:
return Retrier[httpx.Response](
circuit_breaker=test_custom_circuit_breaker_in_memory,
max_retries=MAX_RETRIES,
exceptions_to_retry=(ZeroDivisionError,),
stop_rule=tenacity.stop.stop_after_attempt(MAX_RETRIES),
retry_cause=tenacity.retry_if_exception_type(ZeroDivisionError),
wait_strategy=tenacity.wait_none(),
)
6 changes: 3 additions & 3 deletions tests/test_circuit_breaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from tests.conftest import MAX_RETRIES, SOME_HOST


async def test_circuit_breaker_in_memory(test_circuit_breaker_in_memory: CircuitBreakerInMemory) -> None:
async def test_circuit_breaker_in_memory_cash(test_circuit_breaker_in_memory: CircuitBreakerInMemory) -> None:
assert await test_circuit_breaker_in_memory.is_host_available(host=SOME_HOST)

for _ in range(MAX_RETRIES):
Expand All @@ -19,7 +19,7 @@ async def test_circuit_breaker_in_memory(test_circuit_breaker_in_memory: Circuit
await test_circuit_breaker_in_memory.raise_host_unavailable_error(host=SOME_HOST)


async def test_circuit_breaker_redis(test_circuit_breaker_redis: CircuitBreakerRedis) -> None:
async def test_circuit_breaker_with_redis(test_circuit_breaker_redis: CircuitBreakerRedis) -> None:
assert await test_circuit_breaker_redis.is_host_available(host=SOME_HOST)

for _ in range(MAX_RETRIES):
Expand All @@ -31,7 +31,7 @@ async def test_circuit_breaker_redis(test_circuit_breaker_redis: CircuitBreakerR
await test_circuit_breaker_redis.raise_host_unavailable_error(host=SOME_HOST)


async def test_custom_circuit_breaker_in_memory(
async def test_custom_circuit_breaker_in_memory_cash(
test_custom_circuit_breaker_in_memory: CustomCircuitBreakerInMemory,
) -> None:
assert await test_custom_circuit_breaker_in_memory.is_host_available(host=SOME_HOST)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_retriers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import httpx
import pytest

from circuit_breaker_box import Retrier, RetrierCircuitBreaker
from circuit_breaker_box import Retrier
from tests.conftest import SOME_HOST


Expand All @@ -27,7 +27,7 @@ async def foo(request: httpx.Request) -> typing.NoReturn: # noqa: ARG001


async def test_retry_custom_circuit_breaker(
test_retry_custom_circuit_breaker_in_memory: RetrierCircuitBreaker[httpx.Response],
test_retry_custom_circuit_breaker_in_memory: Retrier[httpx.Response],
) -> None:
test_request = httpx.AsyncClient().build_request(method="GET", url=SOME_HOST)

Expand Down
Loading