Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/backoff.py: 73%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import math
2import random
3from abc import ABC, abstractmethod
5# Maximum backoff between each retry in seconds
6DEFAULT_CAP = 0.512
7# Minimum backoff between each retry in seconds
8DEFAULT_BASE = 0.008
11def _exponential(base: float, failures: int) -> float:
12 """Return ``base * 2**failures``, or infinity once that overflows a float.
14 ``2**failures`` is an arbitrary precision int, and multiplying a float by one
15 beyond the float range raises OverflowError even when the product would fit.
16 ``ldexp`` scales the base directly, so it only overflows when the result
17 itself does, and every strategy below caps an infinite delay.
18 """
19 try:
20 return math.ldexp(base, failures)
21 except OverflowError:
22 return math.inf
25class AbstractBackoff(ABC):
26 """Backoff interface"""
28 def reset(self):
29 """
30 Reset internal state before an operation.
31 `reset` is called once at the beginning of
32 every call to `Retry.call_with_retry`
33 """
34 pass
36 @abstractmethod
37 def compute(self, failures: int) -> float:
38 """Compute backoff in seconds upon failure"""
39 pass
42class ConstantBackoff(AbstractBackoff):
43 """Constant backoff upon failure"""
45 def __init__(self, backoff: float) -> None:
46 """`backoff`: backoff time in seconds"""
47 self._backoff = backoff
49 def __hash__(self) -> int:
50 return hash((self._backoff,))
52 def __eq__(self, other) -> bool:
53 if not isinstance(other, ConstantBackoff):
54 return NotImplemented
56 return self._backoff == other._backoff
58 def compute(self, failures: int) -> float:
59 return self._backoff
62class NoBackoff(ConstantBackoff):
63 """No backoff upon failure"""
65 def __init__(self) -> None:
66 super().__init__(0)
69class ExponentialBackoff(AbstractBackoff):
70 """Exponential backoff upon failure"""
72 def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE):
73 """
74 `cap`: maximum backoff time in seconds
75 `base`: base backoff time in seconds
76 """
77 self._cap = cap
78 self._base = base
80 def __hash__(self) -> int:
81 return hash((self._base, self._cap))
83 def __eq__(self, other) -> bool:
84 if not isinstance(other, ExponentialBackoff):
85 return NotImplemented
87 return self._base == other._base and self._cap == other._cap
89 def compute(self, failures: int) -> float:
90 return min(self._cap, _exponential(self._base, failures))
93class FullJitterBackoff(AbstractBackoff):
94 """Full jitter backoff upon failure"""
96 def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None:
97 """
98 `cap`: maximum backoff time in seconds
99 `base`: base backoff time in seconds
100 """
101 self._cap = cap
102 self._base = base
104 def __hash__(self) -> int:
105 return hash((self._base, self._cap))
107 def __eq__(self, other) -> bool:
108 if not isinstance(other, FullJitterBackoff):
109 return NotImplemented
111 return self._base == other._base and self._cap == other._cap
113 def compute(self, failures: int) -> float:
114 return random.uniform(0, min(self._cap, _exponential(self._base, failures)))
117class EqualJitterBackoff(AbstractBackoff):
118 """Equal jitter backoff upon failure"""
120 def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None:
121 """
122 `cap`: maximum backoff time in seconds
123 `base`: base backoff time in seconds
124 """
125 self._cap = cap
126 self._base = base
128 def __hash__(self) -> int:
129 return hash((self._base, self._cap))
131 def __eq__(self, other) -> bool:
132 if not isinstance(other, EqualJitterBackoff):
133 return NotImplemented
135 return self._base == other._base and self._cap == other._cap
137 def compute(self, failures: int) -> float:
138 temp = min(self._cap, _exponential(self._base, failures)) / 2
139 return temp + random.uniform(0, temp)
142class DecorrelatedJitterBackoff(AbstractBackoff):
143 """Decorrelated jitter backoff upon failure"""
145 def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None:
146 """
147 `cap`: maximum backoff time in seconds
148 `base`: base backoff time in seconds
149 """
150 self._cap = cap
151 self._base = base
152 self._previous_backoff = 0
154 def __hash__(self) -> int:
155 return hash((self._base, self._cap))
157 def __eq__(self, other) -> bool:
158 if not isinstance(other, DecorrelatedJitterBackoff):
159 return NotImplemented
161 return self._base == other._base and self._cap == other._cap
163 def reset(self) -> None:
164 self._previous_backoff = 0
166 def compute(self, failures: int) -> float:
167 max_backoff = max(self._base, self._previous_backoff * 3)
168 temp = random.uniform(self._base, max_backoff)
169 self._previous_backoff = min(self._cap, temp)
170 return self._previous_backoff
173class ExponentialWithJitterBackoff(AbstractBackoff):
174 """Exponential backoff upon failure, with jitter"""
176 def __init__(self, cap: float = DEFAULT_CAP, base: float = DEFAULT_BASE) -> None:
177 """
178 `cap`: maximum backoff time in seconds
179 `base`: base backoff time in seconds
180 """
181 self._cap = cap
182 self._base = base
184 def __hash__(self) -> int:
185 return hash((self._base, self._cap))
187 def __eq__(self, other) -> bool:
188 if not isinstance(other, ExponentialWithJitterBackoff):
189 return NotImplemented
191 return self._base == other._base and self._cap == other._cap
193 def compute(self, failures: int) -> float:
194 return min(self._cap, _exponential(random.random() * self._base, failures))
197def default_backoff():
198 return EqualJitterBackoff()