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