Skip to content

Commit 1cf7df3

Browse files
committed
added module retry
1 parent 6852849 commit 1cf7df3

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import time
2+
import logging
3+
4+
from ..exceptions import (
5+
ProtocolError,
6+
ConnectTimeoutError,
7+
ReadTimeoutError,
8+
MaxRetryError,
9+
)
10+
from ..packages import six
11+
12+
13+
log = logging.getLogger(__name__)
14+
15+
16+
class Retry(object):
17+
""" Retry configuration.
18+
19+
Each retry attempt will create a new Retry object with updated values, so
20+
they can be safely reused.
21+
22+
Retries can be defined as a default for a pool::
23+
24+
retries = Retry(connect=5, read=2, redirect=5)
25+
http = PoolManager(retries=retries)
26+
response = http.request('GET', 'http://example.com/')
27+
28+
Or per-request (which overrides the default for the pool)::
29+
30+
response = http.request('GET', 'http://example.com/', retries=Retry(10))
31+
32+
Retries can be disabled by passing ``False``::
33+
34+
response = http.request('GET', 'http://example.com/', retries=False)
35+
36+
Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless
37+
retries are disabled, in which case the causing exception will be raised.
38+
39+
40+
:param int total:
41+
Total number of retries to allow. Takes precedence over other counts.
42+
43+
Set to ``None`` to remove this constraint and fall back on other
44+
counts. It's a good idea to set this to some sensibly-high value to
45+
account for unexpected edge cases and avoid infinite retry loops.
46+
47+
Set to ``0`` to fail on the first retry.
48+
49+
Set to ``False`` to disable and imply ``raise_on_redirect=False``.
50+
51+
:param int connect:
52+
How many connection-related errors to retry on.
53+
54+
These are errors raised before the request is sent to the remote server,
55+
which we assume has not triggered the server to process the request.
56+
57+
Set to ``0`` to fail on the first retry of this type.
58+
59+
:param int read:
60+
How many times to retry on read errors.
61+
62+
These errors are raised after the request was sent to the server, so the
63+
request may have side-effects.
64+
65+
Set to ``0`` to fail on the first retry of this type.
66+
67+
:param int redirect:
68+
How many redirects to perform. Limit this to avoid infinite redirect
69+
loops.
70+
71+
A redirect is a HTTP response with a status code 301, 302, 303, 307 or
72+
308.
73+
74+
Set to ``0`` to fail on the first retry of this type.
75+
76+
Set to ``False`` to disable and imply ``raise_on_redirect=False``.
77+
78+
:param iterable method_whitelist:
79+
Set of uppercased HTTP method verbs that we should retry on.
80+
81+
By default, we only retry on methods which are considered to be
82+
indempotent (multiple requests with the same parameters end with the
83+
same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`.
84+
85+
:param iterable status_forcelist:
86+
A set of HTTP status codes that we should force a retry on.
87+
88+
By default, this is disabled with ``None``.
89+
90+
:param float backoff_factor:
91+
A backoff factor to apply between attempts. urllib3 will sleep for::
92+
93+
{backoff factor} * (2 ^ ({number of total retries} - 1))
94+
95+
seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep
96+
for [0.1s, 0.2s, 0.4s, ...] between retries. It will never be longer
97+
than :attr:`Retry.MAX_BACKOFF`.
98+
99+
By default, backoff is disabled (set to 0).
100+
101+
:param bool raise_on_redirect: Whether, if the number of redirects is
102+
exhausted, to raise a MaxRetryError, or to return a response with a
103+
response code in the 3xx range.
104+
"""
105+
106+
DEFAULT_METHOD_WHITELIST = frozenset([
107+
'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'])
108+
109+
#: Maximum backoff time.
110+
BACKOFF_MAX = 120
111+
112+
def __init__(self, total=10, connect=None, read=None, redirect=None,
113+
method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None,
114+
backoff_factor=0, raise_on_redirect=True, _observed_errors=0):
115+
116+
self.total = total
117+
self.connect = connect
118+
self.read = read
119+
120+
if redirect is False or total is False:
121+
redirect = 0
122+
raise_on_redirect = False
123+
124+
self.redirect = redirect
125+
self.status_forcelist = status_forcelist or set()
126+
self.method_whitelist = method_whitelist
127+
self.backoff_factor = backoff_factor
128+
self.raise_on_redirect = raise_on_redirect
129+
self._observed_errors = _observed_errors # TODO: use .history instead?
130+
131+
def new(self, **kw):
132+
params = dict(
133+
total=self.total,
134+
connect=self.connect, read=self.read, redirect=self.redirect,
135+
method_whitelist=self.method_whitelist,
136+
status_forcelist=self.status_forcelist,
137+
backoff_factor=self.backoff_factor,
138+
raise_on_redirect=self.raise_on_redirect,
139+
_observed_errors=self._observed_errors,
140+
)
141+
params.update(kw)
142+
return type(self)(**params)
143+
144+
@classmethod
145+
def from_int(cls, retries, redirect=True, default=None):
146+
""" Backwards-compatibility for the old retries format."""
147+
if retries is None:
148+
retries = default if default is not None else cls.DEFAULT
149+
150+
if isinstance(retries, Retry):
151+
return retries
152+
153+
redirect = bool(redirect) and None
154+
new_retries = cls(retries, redirect=redirect)
155+
log.debug("Converted retries value: %r -> %r" % (retries, new_retries))
156+
return new_retries
157+
158+
def get_backoff_time(self):
159+
""" Formula for computing the current backoff
160+
161+
:rtype: float
162+
"""
163+
if self._observed_errors <= 1:
164+
return 0
165+
166+
backoff_value = self.backoff_factor * (2 ** (self._observed_errors - 1))
167+
return min(self.BACKOFF_MAX, backoff_value)
168+
169+
def sleep(self):
170+
""" Sleep between retry attempts using an exponential backoff.
171+
172+
By default, the backoff factor is 0 and this method will return
173+
immediately.
174+
"""
175+
backoff = self.get_backoff_time()
176+
if backoff <= 0:
177+
return
178+
time.sleep(backoff)
179+
180+
def _is_connection_error(self, err):
181+
""" Errors when we're fairly sure that the server did not receive the
182+
request, so it should be safe to retry.
183+
"""
184+
return isinstance(err, ConnectTimeoutError)
185+
186+
def _is_read_error(self, err):
187+
""" Errors that occur after the request has been started, so we can't
188+
assume that the server did not process any of it.
189+
"""
190+
return isinstance(err, (ReadTimeoutError, ProtocolError))
191+
192+
def is_forced_retry(self, method, status_code):
193+
""" Is this method/response retryable? (Based on method/codes whitelists)
194+
"""
195+
if self.method_whitelist and method.upper() not in self.method_whitelist:
196+
return False
197+
198+
return self.status_forcelist and status_code in self.status_forcelist
199+
200+
def is_exhausted(self):
201+
""" Are we out of retries?
202+
"""
203+
retry_counts = (self.total, self.connect, self.read, self.redirect)
204+
retry_counts = list(filter(None, retry_counts))
205+
if not retry_counts:
206+
return False
207+
208+
return min(retry_counts) < 0
209+
210+
def increment(self, method=None, url=None, response=None, error=None, _pool=None, _stacktrace=None):
211+
""" Return a new Retry object with incremented retry counters.
212+
213+
:param response: A response object, or None, if the server did not
214+
return a response.
215+
:type response: :class:`~urllib3.response.HTTPResponse`
216+
:param Exception error: An error encountered during the request, or
217+
None if the response was received successfully.
218+
219+
:return: A new ``Retry`` object.
220+
"""
221+
if self.total is False and error:
222+
# Disabled, indicate to re-raise the error.
223+
raise six.reraise(type(error), error, _stacktrace)
224+
225+
total = self.total
226+
if total is not None:
227+
total -= 1
228+
229+
_observed_errors = self._observed_errors
230+
connect = self.connect
231+
read = self.read
232+
redirect = self.redirect
233+
234+
if error and self._is_connection_error(error):
235+
# Connect retry?
236+
if connect is False:
237+
raise six.reraise(type(error), error, _stacktrace)
238+
elif connect is not None:
239+
connect -= 1
240+
_observed_errors += 1
241+
242+
elif error and self._is_read_error(error):
243+
# Read retry?
244+
if read is False:
245+
raise six.reraise(type(error), error, _stacktrace)
246+
elif read is not None:
247+
read -= 1
248+
_observed_errors += 1
249+
250+
elif response and response.get_redirect_location():
251+
# Redirect retry?
252+
if redirect is not None:
253+
redirect -= 1
254+
255+
else:
256+
# FIXME: Nothing changed, scenario doesn't make sense.
257+
_observed_errors += 1
258+
259+
new_retry = self.new(
260+
total=total,
261+
connect=connect, read=read, redirect=redirect,
262+
_observed_errors=_observed_errors)
263+
264+
if new_retry.is_exhausted():
265+
raise MaxRetryError(_pool, url, error)
266+
267+
log.debug("Incremented Retry for (url='%s'): %r" % (url, new_retry))
268+
269+
return new_retry
270+
271+
272+
def __repr__(self):
273+
return ('{cls.__name__}(total={self.total}, connect={self.connect}, '
274+
'read={self.read}, redirect={self.redirect})').format(
275+
cls=type(self), self=self)
276+
277+
278+
# For backwards compatibility (equivalent to pre-v1.9):
279+
Retry.DEFAULT = Retry(3)

0 commit comments

Comments
 (0)