Plex plugin to to play various online streams (mostly Latvian).

retry.py 10KB

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