Retry & jitter¶
@retry¶
A decorator for exponential-backoff retries. It detects whether the wrapped function is sync or async and picks the right implementation automatically.
from vayu import retry
@retry(ConnectionError, tries=4, delay=1, backoff=2)
async def call_api():
...
exception_to_check— exception class or tuple of classes to catch. Others propagate.tries— total attempts (not retries).tries=4means up to 4 calls.delay— initial sleep between attempts, seconds.backoff— multiplier applied todelayafter each failure.logger— if provided, the retry message goes tologger.warning. Otherwiseprint.
Example effective delays with delay=1, backoff=2, tries=4: 1 s, 2 s, 4 s, then give up and re-raise the last exception.
Multiple exception classes¶
Why the noqa?¶
The class is lowercase (class retry) on purpose — it reads like a decorator at call sites. The # noqa suppresses the class-naming lint.
add_jitter¶
Randomly perturb a value by ±N%:
from vayu.common import add_jitter
sleep_for = add_jitter(30) # 24.0 - 36.0 seconds
sleep_for = add_jitter(30, jitter_percentage=10) # 27.0 - 33.0
Pairs well with @retry when you want to spread retry storms across instances — though note that @retry itself doesn't apply jitter. If you want jittered backoff, apply it in the wrapped function's sleep:
import asyncio
from vayu.common import add_jitter
async def poll_once():
try:
return await fetch()
except ConnectionError:
await asyncio.sleep(add_jitter(5))
raise
Other helpers in common¶
group(iterable, key)¶
from vayu.common import group
users = [{"name": "A", "team": 1}, {"name": "B", "team": 1}, {"name": "C", "team": 2}]
by_team = group(users, key=lambda u: u["team"])
# {1: [{"name": "A", ...}, {"name": "B", ...}], 2: [{"name": "C", ...}]}
camel_or_space_to_snake¶
from vayu import camel_or_space_to_snake
camel_or_space_to_snake("HelloWorld") # "hello_world"
camel_or_space_to_snake("hello world") # "hello_world"