Skip to content

wowori/tryr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tryr

Retries that get out of your way.

A small, focused retry library for Python. Decorators and context managers for sync and async code, with backoff strategies, jitter, hooks, and an optional circuit breaker. Zero required dependencies.

Why

  • tenacity is mature but has 40+ parameters.
  • backoff hasn't been updated since 2019 and async support is awkward.
  • You just want a clean decorator that retries on the right exceptions, sleeps with a sensible backoff, and gets out of your way.

That's tryr.

Install

pip install tryr

Quick start

from tryr import retry

@retry(max_attempts=5, retry_on=(ConnectionError, TimeoutError))
def fetch_user(user_id: int) -> dict:
    return requests.get(f"https://api.example.com/users/{user_id}").json()

Async version:

from tryr import aretry

@aretry(max_attempts=3, backoff="exponential", jitter="full")
async def fetch_async(url: str) -> dict:
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as r:
            return await r.json()

Backoff strategies

@retry(backoff="fixed", initial_delay=1.0)               # 1, 1, 1, ...
@retry(backoff="linear", initial_delay=0.5, max_delay=10) # 0.5, 1.0, 1.5, ...
@retry(backoff="exponential", initial_delay=0.5, max_delay=30)  # 0.5, 1, 2, 4, 8, 16, 30, ...

Add jitter to avoid thundering herd:

@retry(backoff="exponential", jitter="full")   # random in [0, delay]
@retry(backoff="exponential", jitter="equal")  # delay/2 + random(0, delay/2)

Exception classification

By default, tryr retries on Exception if you don't specify retry_on. Be explicit in real code:

@retry(
    retry_on=(requests.RequestException, TimeoutError),
    give_up_on=(requests.HTTPError,),  # 4xx won't be retried
)
def fetch(): ...

Hooks

@retry(
    max_attempts=5,
    on_retry=lambda attempt, exc, delay: logger.warning(
        "attempt %d failed: %s (sleeping %.2fs)", attempt, exc, delay
    ),
    on_give_up=lambda attempts, exc: metrics.counter("retries.give_up").inc(),
)
def f(): ...

Circuit breaker

Stop hammering a dead service:

from tryr import circuit_breaker

@circuit_breaker(failure_threshold=5, reset_timeout=30.0)
@retry(max_attempts=3)
def call_external_api(): ...

After 5 consecutive failures the breaker opens for 30 seconds, during which calls fail fast with CircuitOpenError. Then it half-opens for a probe call.

Context manager

For blocks that aren't functions:

from tryr import Retrier

with Retrier(max_attempts=3, backoff="exponential") as r:
    while r.should_retry():
        try:
            do_thing()
            break
        except TransientError as e:
            r.record_failure(e)

Development

git clone https://github.com/wowori/tryr
cd tryr
python -m venv .venv
.venv\Scripts\activate
pip install -e ".[dev]"
pytest

License

MIT

About

Retries that get out of your way. Sync/async decorators, backoff, jitter, circuit breaker.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages