Skip to content

Commit

Permalink
Del backoff
Browse files Browse the repository at this point in the history
  • Loading branch information
evgeny-stakewise committed Jul 7, 2023
1 parent 84e30c2 commit 5d23e2d
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 400 deletions.
13 changes: 1 addition & 12 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
[tool.poetry]
name = "sw-utils"
version = "0.3.13"
version = "0.3.14"
description = "StakeWise Python utils"
authors = ["StakeWise Labs <[email protected]>"]
license = "GPL-3.0-or-later"
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"
backoff = "^2.2.1"
milagro-bls-binding = "==1.9.0"
py-ecc = "^6.0.0"
ipfshttpclient = "^0.8.0a2"
Expand Down
156 changes: 25 additions & 131 deletions sw_utils/decorators.py
Original file line number Diff line number Diff line change
@@ -1,148 +1,42 @@
import asyncio
import functools
import logging
import typing

import aiohttp
import backoff
import requests
from tenacity import retry, retry_if_exception, stop_after_delay, wait_exponential

from sw_utils.exceptions import (
AiohttpRecoveredErrors,
RecoverableServerError,
RequestsRecoveredErrors,
)
default_logger = logging.getLogger(__name__)

logger = logging.getLogger(__name__)

if typing.TYPE_CHECKING:
from tenacity import RetryCallState

def wrap_aiohttp_500_errors(f):
"""
Allows to distinguish between HTTP 400 and HTTP 500 errors.
Both are represented by `aiohttp.ClientResponseError`.
"""

@functools.wraps(f)
async def wrapper(*args, **kwargs):
try:
return await f(*args, **kwargs)
except aiohttp.ClientResponseError as e:
if e.status >= 500:
raise RecoverableServerError(e) from e
raise
def custom_before_log(logger, log_level):
def custom_log_it(retry_state: 'RetryCallState') -> None:
if retry_state.attempt_number <= 1:
return
msg = 'Retrying %s(), attempt %s'
args = (retry_state.fn.__name__, retry_state.attempt_number) # type: ignore
logger.log(log_level, msg, *args)

return wrapper
return custom_log_it


def backoff_aiohttp_errors(max_tries: int | None = None, max_time: int | None = None, **kwargs):
"""
Can be used for:
* retrying web3 api calls
* retrying aiohttp calls to services
def can_be_retried_aiohttp_error(e: BaseException) -> bool:
if isinstance(e, (asyncio.TimeoutError, aiohttp.ClientConnectionError)):
return True

DO NOT use `backoff_aiohttp_errors` for handling errors in IpfsFetchClient
or IpfsMultiUploadClient.
Catch `sw_utils/ipfs.py#IpfsException` instead.
if isinstance(e, aiohttp.ClientResponseError) and e.status >= 500:
return True

Retry:
* connection errors
* HTTP 500 errors
Do not retry:
* HTTP 400 errors
* regular Python errors
"""
return False

backoff_decorator = backoff.on_exception(
backoff.expo,
AiohttpRecoveredErrors,
max_tries=max_tries,
max_time=max_time,
**kwargs,
)

def decorator(f):
@functools.wraps(f)
async def wrapper(*args, **kwargs):
try:
return await backoff_decorator(wrap_aiohttp_500_errors(f))(*args, **kwargs)
except RecoverableServerError as e:
raise e.origin

return wrapper

return decorator


def wrap_requests_500_errors(f):
"""
Allows to distinguish between HTTP 400 and HTTP 500 errors.
Both are represented by `requests.HTTPError`.
"""
if asyncio.iscoroutinefunction(f):

@functools.wraps(f)
async def async_wrapper(*args, **kwargs):
try:
return await f(*args, **kwargs)
except requests.HTTPError as e:
if e.response.status >= 500:
raise RecoverableServerError(e) from e
raise

return async_wrapper

@functools.wraps(f)
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except requests.HTTPError as e:
if e.response.status >= 500:
raise RecoverableServerError(e) from e
raise

return wrapper


def backoff_requests_errors(max_tries: int | None = None, max_time: int | None = None, **kwargs):
"""
DO NOT use `backoff_requests_errors` for handling errors in IpfsFetchClient
or IpfsMultiUploadClient.
Catch `sw_utils/ipfs.py#IpfsException` instead.
Retry:
* connection errors
* HTTP 500 errors
Do not retry:
* HTTP 400 errors
* regular Python errors
"""

backoff_decorator = backoff.on_exception(
backoff.expo,
RequestsRecoveredErrors,
max_tries=max_tries,
max_time=max_time,
**kwargs,
def retry_aiohttp_errors(delay: int = 60):
return retry(
retry=retry_if_exception(can_be_retried_aiohttp_error),
wait=wait_exponential(multiplier=1, min=1, max=delay // 2),
stop=stop_after_delay(delay),
before=custom_before_log(default_logger, logging.INFO),
)

def decorator(f):
if asyncio.iscoroutinefunction(f):

@functools.wraps(f)
async def async_wrapper(*args, **kwargs):
try:
return await backoff_decorator(wrap_requests_500_errors(f))(*args, **kwargs)
except RecoverableServerError as e:
raise e.origin

return async_wrapper

@functools.wraps(f)
def wrapper(*args, **kwargs):
try:
return backoff_decorator(wrap_requests_500_errors(f))(*args, **kwargs)
except RecoverableServerError as e:
raise e.origin

return wrapper

return decorator
42 changes: 0 additions & 42 deletions sw_utils/tenacity_decorators.py

This file was deleted.

Loading

0 comments on commit 5d23e2d

Please sign in to comment.