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

99 statements  

1import math 

2import random 

3from abc import ABC, abstractmethod 

4 

5# Maximum backoff between each retry in seconds 

6DEFAULT_CAP = 0.512 

7# Minimum backoff between each retry in seconds 

8DEFAULT_BASE = 0.008 

9 

10 

11def _exponential(base: float, failures: int) -> float: 

12 """Return ``base * 2**failures``, or infinity once that overflows a float. 

13 

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 

23 

24 

25class AbstractBackoff(ABC): 

26 """Backoff interface""" 

27 

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 

35 

36 @abstractmethod 

37 def compute(self, failures: int) -> float: 

38 """Compute backoff in seconds upon failure""" 

39 pass 

40 

41 

42class ConstantBackoff(AbstractBackoff): 

43 """Constant backoff upon failure""" 

44 

45 def __init__(self, backoff: float) -> None: 

46 """`backoff`: backoff time in seconds""" 

47 self._backoff = backoff 

48 

49 def __hash__(self) -> int: 

50 return hash((self._backoff,)) 

51 

52 def __eq__(self, other) -> bool: 

53 if not isinstance(other, ConstantBackoff): 

54 return NotImplemented 

55 

56 return self._backoff == other._backoff 

57 

58 def compute(self, failures: int) -> float: 

59 return self._backoff 

60 

61 

62class NoBackoff(ConstantBackoff): 

63 """No backoff upon failure""" 

64 

65 def __init__(self) -> None: 

66 super().__init__(0) 

67 

68 

69class ExponentialBackoff(AbstractBackoff): 

70 """Exponential backoff upon failure""" 

71 

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 

79 

80 def __hash__(self) -> int: 

81 return hash((self._base, self._cap)) 

82 

83 def __eq__(self, other) -> bool: 

84 if not isinstance(other, ExponentialBackoff): 

85 return NotImplemented 

86 

87 return self._base == other._base and self._cap == other._cap 

88 

89 def compute(self, failures: int) -> float: 

90 return min(self._cap, _exponential(self._base, failures)) 

91 

92 

93class FullJitterBackoff(AbstractBackoff): 

94 """Full jitter backoff upon failure""" 

95 

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 

103 

104 def __hash__(self) -> int: 

105 return hash((self._base, self._cap)) 

106 

107 def __eq__(self, other) -> bool: 

108 if not isinstance(other, FullJitterBackoff): 

109 return NotImplemented 

110 

111 return self._base == other._base and self._cap == other._cap 

112 

113 def compute(self, failures: int) -> float: 

114 return random.uniform(0, min(self._cap, _exponential(self._base, failures))) 

115 

116 

117class EqualJitterBackoff(AbstractBackoff): 

118 """Equal jitter backoff upon failure""" 

119 

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 

127 

128 def __hash__(self) -> int: 

129 return hash((self._base, self._cap)) 

130 

131 def __eq__(self, other) -> bool: 

132 if not isinstance(other, EqualJitterBackoff): 

133 return NotImplemented 

134 

135 return self._base == other._base and self._cap == other._cap 

136 

137 def compute(self, failures: int) -> float: 

138 temp = min(self._cap, _exponential(self._base, failures)) / 2 

139 return temp + random.uniform(0, temp) 

140 

141 

142class DecorrelatedJitterBackoff(AbstractBackoff): 

143 """Decorrelated jitter backoff upon failure""" 

144 

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 

153 

154 def __hash__(self) -> int: 

155 return hash((self._base, self._cap)) 

156 

157 def __eq__(self, other) -> bool: 

158 if not isinstance(other, DecorrelatedJitterBackoff): 

159 return NotImplemented 

160 

161 return self._base == other._base and self._cap == other._cap 

162 

163 def reset(self) -> None: 

164 self._previous_backoff = 0 

165 

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 

171 

172 

173class ExponentialWithJitterBackoff(AbstractBackoff): 

174 """Exponential backoff upon failure, with jitter""" 

175 

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 

183 

184 def __hash__(self) -> int: 

185 return hash((self._base, self._cap)) 

186 

187 def __eq__(self, other) -> bool: 

188 if not isinstance(other, ExponentialWithJitterBackoff): 

189 return NotImplemented 

190 

191 return self._base == other._base and self._cap == other._cap 

192 

193 def compute(self, failures: int) -> float: 

194 return min(self._cap, _exponential(random.random() * self._base, failures)) 

195 

196 

197def default_backoff(): 

198 return EqualJitterBackoff()