Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Commit 3664f18

Browse files
Retry requests on ConnectionError and Timeout (#210)
Adds a retry logic to `send_post_request` and `send_put_request` to retry on ConnectionError and Timeout. Also makes the legacy upload sender use those functions. There might be a better way to do that without the custom logic, but the `Retry` option I saw you specify retry error codes, and I'm not sure ECONNRESET gives an error code. But apparently it is a `requests.exceptions.COnnectionError`, so... Fixes uploader #537
1 parent 0193199 commit 3664f18

File tree

4 files changed

+99
-34
lines changed

4 files changed

+99
-34
lines changed

codecov_cli/helpers/request.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
11
import logging
22
import uuid
3+
from time import sleep
34

45
import requests
56

67
from codecov_cli.types import RequestError, RequestResult
78

89
logger = logging.getLogger("codecovcli")
910

11+
MAX_RETRIES = 3
1012

13+
14+
def backoff_time(curr_retry):
15+
return 2 ** (curr_retry - 1)
16+
17+
18+
def retry_request(func):
19+
def wrapper(*args, **kwargs):
20+
retry = 0
21+
while retry < MAX_RETRIES:
22+
try:
23+
return func(*args, **kwargs)
24+
except (
25+
requests.exceptions.ConnectionError,
26+
requests.exceptions.Timeout,
27+
) as exp:
28+
logger.warning(
29+
"Request failed. Retrying",
30+
extra=dict(extra_log_attributes=dict(retry=retry)),
31+
)
32+
sleep(backoff_time(retry))
33+
retry += 1
34+
raise Exception("Request failed after too many retries")
35+
36+
return wrapper
37+
38+
39+
@retry_request
1140
def send_post_request(
12-
url: str,
13-
data: dict = None,
14-
headers: dict = None,
41+
url: str, data: dict = None, headers: dict = None, params: dict = None
1542
):
16-
resp = requests.post(
17-
url=url,
18-
json=data,
19-
headers=headers,
20-
)
21-
43+
resp = requests.post(url=url, json=data, headers=headers, params=params)
2244
return request_result(resp)
2345

2446

@@ -30,6 +52,7 @@ def get_token_header_or_fail(token: uuid.UUID) -> dict:
3052
return {"Authorization": f"token {token.hex}"}
3153

3254

55+
@retry_request
3356
def send_put_request(
3457
url: str,
3558
data: dict = None,

codecov_cli/services/upload/legacy_upload_sender.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from codecov_cli import __version__ as codecov_cli_version
99
from codecov_cli.helpers.config import LEGACY_CODECOV_API_URL
10+
from codecov_cli.helpers.request import send_post_request, send_put_request
1011
from codecov_cli.types import UploadCollectionResult, UploadCollectionResultFile
1112

1213
logger = logging.getLogger("codecovcli")
@@ -75,34 +76,16 @@ def send_upload_data(
7576
headers = {"X-Upload-Token": ""}
7677

7778
upload_url = enterprise_url or LEGACY_CODECOV_API_URL
78-
resp = requests.post(f"{upload_url}/upload/v4", headers=headers, params=params)
79-
79+
resp = send_post_request(
80+
f"{upload_url}/upload/v4", headers=headers, params=params
81+
)
8082
if resp.status_code >= 400:
81-
return UploadSendingResult(
82-
error=UploadSendingError(
83-
code=f"HTTP Error {resp.status_code}",
84-
description=resp.text,
85-
params={},
86-
),
87-
warnings=[],
88-
)
89-
83+
return resp
9084
result_url, put_url = resp.text.split("\n")
9185

9286
reports_payload = self._generate_payload(upload_data, env_vars)
93-
resp = requests.put(put_url, data=reports_payload)
94-
95-
if resp.status_code >= 400:
96-
return UploadSendingResult(
97-
error=UploadSendingError(
98-
code=f"HTTP Error {resp.status_code}",
99-
description=resp.text,
100-
params={},
101-
),
102-
warnings=[],
103-
)
104-
105-
return UploadSendingResult(error=None, warnings=[])
87+
resp = send_put_request(put_url, data=reports_payload)
88+
return resp
10689

10790
def _generate_payload(
10891
self, upload_data: UploadCollectionResult, env_vars: typing.Dict[str, str]

tests/helpers/test_legacy_upload_sender.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ def mocked_legacy_upload_endpoint(mocked_responses):
4545
yield resp
4646

4747

48+
@pytest.fixture
49+
def mocked_legacy_upload_endpoint_too_many_fails(mocked_responses):
50+
resp = responses.Response(
51+
responses.POST,
52+
"https://codecov.io/upload/v4",
53+
body="https://resulturl.com\nhttps://puturl.com",
54+
status=400,
55+
)
56+
for _ in range(4):
57+
mocked_responses.add(resp)
58+
59+
4860
@pytest.fixture
4961
def mocked_storage_server(mocked_responses):
5062
resp = responses.Response(responses.PUT, "https://puturl.com", status=200)
@@ -116,9 +128,10 @@ def test_upload_sender_result_success(
116128
assert not sender.warnings
117129

118130
def test_upload_sender_result_fail_post_400(
119-
self, mocked_responses, mocked_legacy_upload_endpoint
131+
self, mocked_responses, mocked_legacy_upload_endpoint, mocker
120132
):
121133
mocked_legacy_upload_endpoint.status = 400
134+
mocker.patch("codecov_cli.helpers.request.sleep")
122135

123136
sender = LegacyUploadSender().send_upload_data(
124137
upload_collection, random_sha, random_token, {}, **named_upload_data

tests/helpers/test_request.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import uuid
22

33
import pytest
4+
import requests
5+
from requests import Response
46

57
from codecov_cli.helpers.request import (
68
get_token_header_or_fail,
79
log_warnings_and_errors_if_any,
810
)
911
from codecov_cli.helpers.request import logger as req_log
12+
from codecov_cli.helpers.request import request_result, send_post_request
1013
from codecov_cli.types import RequestError, RequestResult
1114

1215

16+
@pytest.fixture
17+
def valid_response():
18+
valid_response = Response()
19+
valid_response.status_code = 200
20+
valid_response._content = b"response text"
21+
return valid_response
22+
23+
1324
def test_log_error_no_raise(mocker):
1425
mock_log_error = mocker.patch.object(req_log, "error")
1526
error = RequestError(
@@ -54,3 +65,38 @@ def test_get_token_header_or_fail():
5465
get_token_header_or_fail(token)
5566

5667
assert str(e.value) == f"Token must be UUID. Received {type(token)}"
68+
69+
70+
def test_request_retry(mocker, valid_response):
71+
expected_response = request_result(valid_response)
72+
mock_sleep = mocker.patch("codecov_cli.helpers.request.sleep")
73+
mocker.patch.object(
74+
requests,
75+
"post",
76+
side_effect=[
77+
requests.exceptions.ConnectionError(),
78+
requests.exceptions.Timeout(),
79+
valid_response,
80+
],
81+
)
82+
resp = send_post_request("my_url")
83+
assert resp == expected_response
84+
mock_sleep.assert_called()
85+
86+
87+
def test_request_retry_too_many_errors(mocker):
88+
mock_sleep = mocker.patch("codecov_cli.helpers.request.sleep")
89+
mocker.patch.object(
90+
requests,
91+
"post",
92+
side_effect=[
93+
requests.exceptions.ConnectionError(),
94+
requests.exceptions.Timeout(),
95+
requests.exceptions.Timeout(),
96+
requests.exceptions.Timeout(),
97+
requests.exceptions.Timeout(),
98+
],
99+
)
100+
with pytest.raises(Exception) as exp:
101+
resp = send_post_request("my_url")
102+
assert str(exp.value) == "Request failed after too many retries"

0 commit comments

Comments
 (0)