Source code for redis.backoff

import math
import random
from abc import ABC, abstractmethod

# Maximum backoff between each retry in seconds
DEFAULT_CAP = 0.512
# Minimum backoff between each retry in seconds
DEFAULT_BASE = 0.008


def _exponential(base: float, failures: int) -> float:
    """Return ``base * 2**failures``, or infinity once that overflows a float.

    ``2**failures`` is an arbitrary precision int, and multiplying a float by one
    beyond the float range raises OverflowError even when the product would fit.
    ``ldexp`` scales the base directly, so it only overflows when the result
    itself does, and every strategy below caps an infinite delay.
    """
    try:
        return math.ldexp(base, failures)
    except OverflowError:
        return math.inf


[docs]class AbstractBackoff(ABC): """Backoff interface"""
[docs] def reset(self): """ Reset internal state before an operation. `reset` is called once at the beginning of every call to `Retry.call_with_retry` """ pass
[docs] @abstractmethod def compute(self, failures: int) -> float: """Compute backoff in seconds upon failure""" pass
[docs]class ConstantBackoff(AbstractBackoff): """Constant backoff upon failure""" def __init__(self, backoff: float) -> None: """`backoff`: backoff time in seconds""" self._backoff = backoff def __hash__(self) -> int: return hash((self._backoff,)) def __eq__(self, other) -> bool: if not isinstance(other, ConstantBackoff): return NotImplemented return self._backoff == other._backoff
[docs] def compute(self, failures: int) -> float: return self._backoff
[docs]class NoBackoff(ConstantBackoff): """No backoff upon failure""" def __init__(self) -> None: super().__init__(0)
[docs]class ExponentialBackoff(AbstractBackoff): """Exponential backoff upon failure""" def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE): """ `cap`: maximum backoff time in seconds `base`: base backoff time in seconds """ self._cap = cap self._base = base def __hash__(self) -> int: return hash((self._base, self._cap)) def __eq__(self, other) -> bool: if not isinstance(other, ExponentialBackoff): return NotImplemented return self._base == other._base and self._cap == other._cap
[docs] def compute(self, failures: int) -> float: return min(self._cap, _exponential(self._base, failures))
[docs]class FullJitterBackoff(AbstractBackoff): """Full jitter backoff upon failure""" def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None: """ `cap`: maximum backoff time in seconds `base`: base backoff time in seconds """ self._cap = cap self._base = base def __hash__(self) -> int: return hash((self._base, self._cap)) def __eq__(self, other) -> bool: if not isinstance(other, FullJitterBackoff): return NotImplemented return self._base == other._base and self._cap == other._cap
[docs] def compute(self, failures: int) -> float: return random.uniform(0, min(self._cap, _exponential(self._base, failures)))
[docs]class EqualJitterBackoff(AbstractBackoff): """Equal jitter backoff upon failure""" def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None: """ `cap`: maximum backoff time in seconds `base`: base backoff time in seconds """ self._cap = cap self._base = base def __hash__(self) -> int: return hash((self._base, self._cap)) def __eq__(self, other) -> bool: if not isinstance(other, EqualJitterBackoff): return NotImplemented return self._base == other._base and self._cap == other._cap
[docs] def compute(self, failures: int) -> float: temp = min(self._cap, _exponential(self._base, failures)) / 2 return temp + random.uniform(0, temp)
[docs]class DecorrelatedJitterBackoff(AbstractBackoff): """Decorrelated jitter backoff upon failure""" def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None: """ `cap`: maximum backoff time in seconds `base`: base backoff time in seconds """ self._cap = cap self._base = base self._previous_backoff = 0 def __hash__(self) -> int: return hash((self._base, self._cap)) def __eq__(self, other) -> bool: if not isinstance(other, DecorrelatedJitterBackoff): return NotImplemented return self._base == other._base and self._cap == other._cap
[docs] def reset(self) -> None: self._previous_backoff = 0
[docs] def compute(self, failures: int) -> float: max_backoff = max(self._base, self._previous_backoff * 3) temp = random.uniform(self._base, max_backoff) self._previous_backoff = min(self._cap, temp) return self._previous_backoff
[docs]class ExponentialWithJitterBackoff(AbstractBackoff): """Exponential backoff upon failure, with jitter""" def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None: """ `cap`: maximum backoff time in seconds `base`: base backoff time in seconds """ self._cap = cap self._base = base def __hash__(self) -> int: return hash((self._base, self._cap)) def __eq__(self, other) -> bool: if not isinstance(other, ExponentialWithJitterBackoff): return NotImplemented return self._base == other._base and self._cap == other._cap
[docs] def compute(self, failures: int) -> float: return min(self._cap, _exponential(random.random() * self._base, failures))
def default_backoff(): return EqualJitterBackoff()