Source code for trader.daemon.backoff
"""Exponential backoff after a failed cycle.
Pure by design: this computes delays and never sleeps. The loop owns sleeping,
which is what lets the growth sequence be asserted directly instead of being
inferred from elapsed time.
"""
__all__ = ["Backoff"]
[docs]
class Backoff:
"""Doubling delay with a ceiling, reset by a success.
Args:
base_seconds: the delay after the first failure.
max_seconds: the ceiling. Must not be below `base_seconds` — a lower
ceiling makes every delay equal to the ceiling, so the delay never
grows and the backoff is decorative.
"""
def __init__(self, *, base_seconds: int, max_seconds: int) -> None:
if base_seconds < 1:
raise ValueError(f"base_seconds must be at least 1, got {base_seconds}.")
if max_seconds < base_seconds:
raise ValueError(
f"max_seconds ({max_seconds}) is below base_seconds "
f"({base_seconds}): the delay would never grow."
)
self.base_seconds = base_seconds
self.max_seconds = max_seconds
self._failures = 0
@property
def consecutive_failures(self) -> int:
"""How many failures since the last success. For the log line."""
return self._failures
[docs]
def next_delay(self) -> int:
"""Record a failure and return how long to wait before retrying.
The exponent is clamped before the shift rather than after. A daemon
that has been failing all day would otherwise compute `base * 2**2000`
— a perfectly valid Python integer that costs real time and memory to
build, only to be thrown away by `min`.
"""
capped_exponent = min(self._failures, self._max_useful_exponent())
delay = min(self.base_seconds * (2**capped_exponent), self.max_seconds)
self._failures += 1
return delay
[docs]
def reset(self) -> None:
"""Forget the failure streak. Called after a successful cycle."""
self._failures = 0
def _max_useful_exponent(self) -> int:
"""The smallest exponent that already reaches the ceiling."""
exponent = 0
while self.base_seconds * (2**exponent) < self.max_seconds:
exponent += 1
return exponent