Commit c22052cb by Arun Babu Neelicattu Committed by GitHub

Add humble retry logic for package downloads (#2813)

parent 8b8b97a6
import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import requests
import requests.auth
import requests.exceptions
from poetry.exceptions import PoetryException
from poetry.utils._compat import urlparse from poetry.utils._compat import urlparse
from poetry.utils.password_manager import PasswordManager from poetry.utils.password_manager import PasswordManager
...@@ -10,10 +17,6 @@ if TYPE_CHECKING: ...@@ -10,10 +17,6 @@ if TYPE_CHECKING:
from typing import Tuple from typing import Tuple
from clikit.api.io import IO from clikit.api.io import IO
from requests import Request # noqa
from requests import Response # noqa
from requests import Session # noqa
from poetry.config.config import Config from poetry.config.config import Config
...@@ -26,24 +29,22 @@ class Authenticator(object): ...@@ -26,24 +29,22 @@ class Authenticator(object):
self._password_manager = PasswordManager(self._config) self._password_manager = PasswordManager(self._config)
@property @property
def session(self): # type: () -> Session def session(self): # type: () -> requests.Session
from requests import Session # noqa
if self._session is None: if self._session is None:
self._session = Session() self._session = requests.Session()
return self._session return self._session
def request(self, method, url, **kwargs): # type: (str, str, Any) -> Response def request(
from requests import Request # noqa self, method, url, **kwargs
from requests.auth import HTTPBasicAuth ): # type: (str, str, Any) -> requests.Response
request = requests.Request(method, url)
request = Request(method, url) io = kwargs.get("io", self._io)
username, password = self._get_credentials_for_url(url) username, password = self._get_credentials_for_url(url)
if username is not None and password is not None: if username is not None and password is not None:
request = HTTPBasicAuth(username, password)(request) request = requests.auth.HTTPBasicAuth(username, password)(request)
session = self.session session = self.session
prepared_request = session.prepare_request(request) prepared_request = session.prepare_request(request)
...@@ -63,12 +64,33 @@ class Authenticator(object): ...@@ -63,12 +64,33 @@ class Authenticator(object):
"allow_redirects": kwargs.get("allow_redirects", True), "allow_redirects": kwargs.get("allow_redirects", True),
} }
send_kwargs.update(settings) send_kwargs.update(settings)
resp = session.send(prepared_request, **send_kwargs)
resp.raise_for_status() attempt = 0
while True:
is_last_attempt = attempt >= 5
try:
resp = session.send(prepared_request, **send_kwargs)
except (requests.exceptions.ConnectionError, OSError) as e:
if is_last_attempt:
raise e
else:
if resp.status_code not in [502, 503, 504] or is_last_attempt:
resp.raise_for_status()
return resp return resp
if not is_last_attempt:
attempt += 1
delay = 0.5 * attempt
io.write_line(
"<debug>Retrying HTTP request in {} seconds.</debug>".format(delay)
)
time.sleep(delay)
continue
# this should never really be hit under any sane circumstance
raise PoetryException("Failed HTTP {} request", method.upper())
def _get_credentials_for_url( def _get_credentials_for_url(
self, url self, url
): # type: (str) -> Tuple[Optional[str], Optional[str]] ): # type: (str) -> Tuple[Optional[str], Optional[str]]
......
...@@ -598,7 +598,9 @@ class Executor(object): ...@@ -598,7 +598,9 @@ class Executor(object):
return archive return archive
def _download_archive(self, operation, link): # type: (Operation, Link) -> Path def _download_archive(self, operation, link): # type: (Operation, Link) -> Path
response = self._authenticator.request("get", link.url, stream=True) response = self._authenticator.request(
"get", link.url, stream=True, io=self._sections.get(id(operation))
)
wheel_size = response.headers.get("content-length") wheel_size = response.headers.get("content-length")
operation_message = self.get_operation_message(operation) operation_message = self.get_operation_message(operation)
message = " <fg=blue;options=bold>•</> {message}: <info>Downloading...</>".format( message = " <fg=blue;options=bold>•</> {message}: <info>Downloading...</>".format(
......
import re import re
import uuid
import httpretty
import pytest import pytest
import requests
from poetry.installation.authenticator import Authenticator from poetry.installation.authenticator import Authenticator
from poetry.io.null_io import NullIO from poetry.io.null_io import NullIO
...@@ -113,3 +116,67 @@ def test_authenticator_uses_empty_strings_as_default_username( ...@@ -113,3 +116,67 @@ def test_authenticator_uses_empty_strings_as_default_username(
request = http.last_request() request = http.last_request()
assert "Basic OmJhcg==" == request.headers["Authorization"] assert "Basic OmJhcg==" == request.headers["Authorization"]
def test_authenticator_request_retries_on_exception(mocker, config, http):
sleep = mocker.patch("time.sleep")
sdist_uri = "https://foo.bar/files/{}/foo-0.1.0.tar.gz".format(str(uuid.uuid4()))
content = str(uuid.uuid4())
seen = list()
def callback(request, uri, response_headers):
if seen.count(uri) < 2:
seen.append(uri)
raise requests.exceptions.ConnectionError("Disconnected")
return [200, response_headers, content]
httpretty.register_uri(httpretty.GET, sdist_uri, body=callback)
authenticator = Authenticator(config, NullIO())
response = authenticator.request("get", sdist_uri)
assert response.text == content
assert sleep.call_count == 2
def test_authenticator_request_raises_exception_when_attempts_exhausted(
mocker, config, http
):
sleep = mocker.patch("time.sleep")
sdist_uri = "https://foo.bar/files/{}/foo-0.1.0.tar.gz".format(str(uuid.uuid4()))
def callback(*_, **__):
raise requests.exceptions.ConnectionError(str(uuid.uuid4()))
httpretty.register_uri(httpretty.GET, sdist_uri, body=callback)
authenticator = Authenticator(config, NullIO())
with pytest.raises(requests.exceptions.ConnectionError):
authenticator.request("get", sdist_uri)
assert sleep.call_count == 5
@pytest.mark.parametrize(
"status, attempts",
[(400, 0), (401, 0), (403, 0), (404, 0), (500, 0), (502, 5), (503, 5), (504, 5)],
)
def test_authenticator_request_retries_on_status_code(
mocker, config, http, status, attempts
):
sleep = mocker.patch("time.sleep")
sdist_uri = "https://foo.bar/files/{}/foo-0.1.0.tar.gz".format(str(uuid.uuid4()))
content = str(uuid.uuid4())
def callback(request, uri, response_headers):
return [status, response_headers, content]
httpretty.register_uri(httpretty.GET, sdist_uri, body=callback)
authenticator = Authenticator(config, NullIO())
with pytest.raises(requests.exceptions.HTTPError) as excinfo:
authenticator.request("get", sdist_uri)
assert excinfo.value.response.status_code == status
assert excinfo.value.response.text == content
assert sleep.call_count == attempts
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment