| """Retry utilities — jittered backoff for decorrelated retries. |
| |
| Replaces fixed exponential backoff with jittered delays to prevent |
| thundering-herd retry spikes when multiple sessions hit the same |
| rate-limited provider concurrently. |
| """ |
|
|
| import random |
| import threading |
| import time |
|
|
| |
| |
| |
| _jitter_counter = 0 |
| _jitter_lock = threading.Lock() |
|
|
|
|
| def jittered_backoff( |
| attempt: int, |
| *, |
| base_delay: float = 5.0, |
| max_delay: float = 120.0, |
| jitter_ratio: float = 0.5, |
| ) -> float: |
| """Compute a jittered exponential backoff delay. |
| |
| Args: |
| attempt: 1-based retry attempt number. |
| base_delay: Base delay in seconds for attempt 1. |
| max_delay: Maximum delay cap in seconds. |
| jitter_ratio: Fraction of computed delay to use as random jitter |
| range. 0.5 means jitter is uniform in [0, 0.5 * delay]. |
| |
| Returns: |
| Delay in seconds: min(base * 2^(attempt-1), max_delay) + jitter. |
| |
| The jitter decorrelates concurrent retries so multiple sessions |
| hitting the same provider don't all retry at the same instant. |
| """ |
| global _jitter_counter |
| with _jitter_lock: |
| _jitter_counter += 1 |
| tick = _jitter_counter |
|
|
| exponent = max(0, attempt - 1) |
| if exponent >= 63 or base_delay <= 0: |
| delay = max_delay |
| else: |
| delay = min(base_delay * (2 ** exponent), max_delay) |
|
|
| |
| seed = (time.time_ns() ^ (tick * 0x9E3779B9)) & 0xFFFFFFFF |
| rng = random.Random(seed) |
| jitter = rng.uniform(0, jitter_ratio * delay) |
|
|
| return delay + jitter |
|
|