40 lines
1.3 KiB
Python
40 lines
1.3 KiB
Python
"""Bounded retry with exponential backoff + full jitter.
|
|
|
|
Fixes subtle bug in prior retry: jitter must be uniform over [0, cap] where cap
|
|
is min(max_delay, base_delay*2**attempt), and a zero/negative sleep must never
|
|
be passed to a clock that rejects negatives. Also re-raises the ORIGINAL
|
|
exception instance (with its traceback) rather than a re-wrapped one.
|
|
"""
|
|
from __future__ import annotations
|
|
import random, time
|
|
from typing import Callable, Optional, Tuple, Type
|
|
|
|
|
|
class RetryExhausted(Exception):
|
|
pass
|
|
|
|
|
|
def retry(fn, *, max_attempts=3, base_delay=0.1, max_delay=10.0,
|
|
retryable=(Exception,), sleep=time.sleep, rng=None):
|
|
if max_attempts < 1:
|
|
raise ValueError("max_attempts must be >= 1")
|
|
if base_delay < 0 or max_delay < 0:
|
|
raise ValueError("delays must be >= 0")
|
|
r = rng if rng is not None else random.Random()
|
|
last = None
|
|
for attempt in range(max_attempts):
|
|
try:
|
|
return fn()
|
|
except retryable as e:
|
|
last = e
|
|
if attempt == max_attempts - 1:
|
|
break
|
|
exp = min(max_delay, base_delay * (2 ** attempt))
|
|
delay = r.uniform(0.0, exp) if exp > 0 else 0.0
|
|
if delay < 0:
|
|
delay = 0.0
|
|
sleep(delay)
|
|
if last is None:
|
|
raise RetryExhausted()
|
|
raise last
|