Skip to content

Commit

Permalink
feat: first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
BobTheBuidler committed Apr 28, 2022
0 parents commit 2bde538
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

# Minimum sleep time in seconds. Integer. Defaults to 10.
MIN_SLEEP_TIME=10

# Maximum sleep time in seconds. Integer. Defaults to 20.
MAX_SLEEP_TIME=20

# Maximum number of times to retry. Integer. Defaults to 10.
MAX_RETRIES=10
25 changes: 25 additions & 0 deletions .github/mypy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: MyPy

on: pull_request

jobs:
deploy:
runs-on: ubuntu-20.04

steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0

- name: Setup Python (faster than using Python container)
uses: actions/setup-python@v2
with:
python-version: "3.9"

- name: Install MyPy
run: |
python -m pip install --upgrade pip
pip install mypy
- name: Run MyPy
run: mypy ./eth_retry --strict --pretty
30 changes: 30 additions & 0 deletions .github/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Upload Python Package

on:
push:
branches:
- dev
release:
branches:
- master

jobs:
deploy:
runs-on: ubuntu-20.04

steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-python@v2
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
build
dist
.eggs
eth_retry.egg-info
__pycache__
env
.mypy_cache
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@

# eth_retry
> Stop transient errors from wasting your time!
`eth_retry` is a Python library that provides one decorator, `eth_retry.auto_retry`.

`auto_retry` will automatically catch known transient exceptions that are common in the Ethereum/EVM ecosystem and will reattempt to evaluate your decorated function up to `os.environ['MAX_RETRIES']` (default: 10) times.

------------

Covers many common transient errors in the EVM ecosystem, including:
- RPC timeouts
- Block explorer API rate-limiting
- Generic exceptions:
- ConnectionError
- HTTPError
- ReadTimeout
- TimeoutError
- eth-brownie specific errors:
- sqlite3.OperationalError: database is locked

## Installation:
`pip install eth_retry`
or
`pip install git+https://github.com/BobTheBuidler/eth_retry.git`

## Usage:
```
import eth_retry
@eth_retry.auto_retry
def some_function_that_errors_sometimes():
i = 0
am = 1
doing = 2
stuff = 3
print("Woohoo it worked!")
return stuff
error_free_result = some_function_that_errors_sometimes()
```

Between attempts, eth_retry will `time.sleep` for a random period between `os.environ['MIN_SLEEP_TIME']` (default: 10) and `os.environ['MAX_SLEEP_TIME']` (default: 20) seconds. The period is randomized to help prevent repetitive rate-limiting issues with parallelism by staggering the retries.

On the `n`th retry, the sleep period is multiplied by `n` so that the target endpoint can cool off in case of rate-limiting.

After `os.environ['MAX_RETRIES']` failures, eth_retry will raise the exception.

## Environment:
```
# Minimum sleep time in seconds. Integer. Defaults to 10.
MIN_SLEEP_TIME=10
# Maximum sleep time in seconds. Integer. Defaults to 20.
MAX_SLEEP_TIME=20
# Maximum number of times to retry. Integer. Defaults to 10.
MAX_RETRIES=10
```
78 changes: 78 additions & 0 deletions eth_retry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import functools
import logging
import os
from random import randrange
from time import sleep
from typing import Any, Callable

from eth_retry.conditional_imports import (HTTPError, # type: ignore
OperationalError, ReadTimeout)

logger = logging.getLogger('eth_retry')

MIN_SLEEP_TIME = int(os.environ.get("MIN_SLEEP_TIME", 10))
MAX_SLEEP_TIME = int(os.environ.get("MAX_SLEEP_TIME", 20))
MAX_RETRIES = int(os.environ.get("MAX_RETRIES", 10))


def auto_retry(func: Callable[...,Any]) -> Callable[...,Any]:
'''
Decorator that will retry the function on:
- ConnectionError
- HTTPError
- TimeoutError
- ReadTimeout
It will also retry on specific ValueError exceptions:
- Max rate limit reached
- please use API Key for higher rate limit
- execution aborted (timeout = 5s)
On repeat errors, will retry in increasing intervals.
'''

@functools.wraps(func)
def auto_retry_wrap(*args: Any, **kwargs: Any) -> Any:
failures = 0
sleep_time = randrange(MIN_SLEEP_TIME,MAX_SLEEP_TIME)
while True:

# Attempt to execute `func` and return response
try:
return func(*args, **kwargs)

# Generic exceptions
except (ConnectionError, HTTPError, TimeoutError, ReadTimeout) as e:
# This happens when we pass too large of a request to the node. Do not retry.
if failures > MAX_RETRIES or 'Too Large' in str(e) or '404' in str(e):
raise
logger.warning(f'{str(e)} [{failures}]')

# Specific exceptions
except OperationalError as e:
# This happens when brownie's deployments.db gets locked. Just retry.
if failures > MAX_RETRIES or 'database is locked' not in str(e):
raise
logger.warning(f'{str(e)} [{failures}]')
except ValueError as e:
retry_on_errs = (
# Occurs on any chain when making computationally intensive calls. Just retry.
# Sometimes works, sometimes doesn't. Worth a shot.
'execution aborted (timeout = 5s)',

# From block explorer while interacting with api. Just retry.
'Max rate limit reached',
'please use API Key for higher rate limit',

# Occurs occasionally on AVAX when node is slow to sync. Just retry.
'after last accepted block',
)
if failures > MAX_RETRIES or not any(err in str(e) for err in retry_on_errs):
raise
logger.warning(f'{str(e)} [{failures}]')

# Attempt failed, sleep time
failures += 1
sleep(failures * sleep_time)

return auto_retry_wrap
13 changes: 13 additions & 0 deletions eth_retry/conditional_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

class DummyException(Exception):
pass

try:
from sqlite3 import OperationalError
except ModuleNotFoundError:
OperationalError = DummyException # type: ignore

try:
from requests.exceptions import HTTPError, ReadTimeout
except ModuleNotFoundError:
HTTPError, ReadTimeout = DummyException, DummyException # type: ignore
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[metadata]
description-file = README.md
20 changes: 20 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from setuptools import find_packages, setup

setup(
name='eth_retry',
packages=find_packages(),
use_scm_version={
"root": ".",
"relative_to": __file__,
"local_scheme": "no-local-version",
"version_scheme": "python-simplified-semver",
},
description='Provides a decorator that automatically catches known transient exceptions that are common in the Ethereum/EVM ecosystem and reattempts to evaluate your decorated function',
author='BobTheBuidler',
author_email='bobthebuidlerdefi@gmail.com',
url='https://github.com/BobTheBuidler/eth_retry',
license='MIT',
setup_requires=[
'setuptools_scm',
],
)

0 comments on commit 2bde538

Please sign in to comment.