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