Skip to content

Commit 147546f

Browse files
paulcruz74facebook-github-bot
authored andcommitted
Add retries to ArchiveFetcher
Summary: X-link: facebookincubator/zstrong#1227 Add retries to ArchiveFetcher when downloading fails. There will be 4 retries, with backoff and jitter. The max delay is capped at 10 seconds. Reviewed By: srikrishnagopu Differential Revision: D71167342 fbshipit-source-id: d927a639cf99185c5a04d063400bdab874dfddfe
1 parent 18873d6 commit 147546f

File tree

2 files changed

+182
-1
lines changed

2 files changed

+182
-1
lines changed

build/fbcode_builder/getdeps/fetcher.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import errno
1010
import hashlib
1111
import os
12+
import random
1213
import re
1314
import shutil
1415
import stat
@@ -837,7 +838,20 @@ def _download_dir(self):
837838

838839
def _download(self) -> None:
839840
self._download_dir()
840-
download_url_to_file_with_progress(self.url, self.file_name)
841+
max_attempts = 5
842+
delay = 1
843+
for attempt in range(max_attempts):
844+
try:
845+
download_url_to_file_with_progress(self.url, self.file_name)
846+
break
847+
except TransientFailure as tf:
848+
if attempt < max_attempts - 1:
849+
delay *= 2
850+
delay_with_jitter = delay * (1 + random.random() * 0.1)
851+
time.sleep(min(delay_with_jitter, 10))
852+
else:
853+
print(f"Failed after retries: {tf}")
854+
raise
841855
self._verify_hash()
842856

843857
def clean(self) -> None:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
# pyre-unsafe
7+
8+
9+
import unittest
10+
from unittest.mock import call, MagicMock, patch
11+
12+
from ..buildopts import BuildOptions
13+
14+
from ..errors import TransientFailure
15+
16+
from ..fetcher import ArchiveFetcher
17+
from ..manifest import ManifestParser
18+
19+
20+
class RetryTest(unittest.TestCase):
21+
def _get_build_opts(self) -> BuildOptions:
22+
mock_build_opts = MagicMock(spec=BuildOptions)
23+
mock_build_opts.scratch_dir = "/path/to/scratch_dir"
24+
return mock_build_opts
25+
26+
def _get_manifest(self) -> ManifestParser:
27+
mock_manifest_parser = MagicMock(spec=ManifestParser)
28+
mock_manifest_parser.name = "mock_manifest_parser"
29+
return mock_manifest_parser
30+
31+
def _get_archive_fetcher(self) -> ArchiveFetcher:
32+
return ArchiveFetcher(
33+
build_options=self._get_build_opts(),
34+
manifest=self._get_manifest(),
35+
url="https://github.com/systemd/systemd/archive/refs/tags/v256.7.tar.gz",
36+
sha256="896d76ff65c88f5fd9e42f90d152b0579049158a163431dd77cdc57748b1d7b0",
37+
)
38+
39+
@patch("os.makedirs")
40+
@patch("os.environ.get")
41+
@patch("time.sleep")
42+
@patch("subprocess.run")
43+
def test_no_retries(
44+
self, mock_run, mock_sleep, mock_os_environ_get, mock_makedirs
45+
) -> None:
46+
def custom_makedirs(path, exist_ok=False):
47+
return None
48+
49+
def custom_get(key, default=None):
50+
if key == "GETDEPS_USE_WGET":
51+
return "1"
52+
elif key == "GETDEPS_WGET_ARGS":
53+
return ""
54+
else:
55+
return None
56+
57+
mock_makedirs.side_effect = custom_makedirs
58+
mock_os_environ_get.side_effect = custom_get
59+
mock_sleep.side_effect = None
60+
fetcher = self._get_archive_fetcher()
61+
fetcher._verify_hash = MagicMock(return_value=None)
62+
fetcher._download()
63+
mock_sleep.assert_has_calls([], any_order=False)
64+
mock_run.assert_called_once_with(
65+
[
66+
"wget",
67+
"-O",
68+
"/path/to/scratch_dir/downloads/mock_manifest_parser-v256.7.tar.gz",
69+
"https://github.com/systemd/systemd/archive/refs/tags/v256.7.tar.gz",
70+
],
71+
capture_output=True,
72+
)
73+
74+
@patch("random.random")
75+
@patch("os.makedirs")
76+
@patch("os.environ.get")
77+
@patch("time.sleep")
78+
@patch("subprocess.run")
79+
def test_retries(
80+
self, mock_run, mock_sleep, mock_os_environ_get, mock_makedirs, mock_random
81+
) -> None:
82+
def custom_makedirs(path, exist_ok=False):
83+
return None
84+
85+
def custom_get(key, default=None):
86+
if key == "GETDEPS_USE_WGET":
87+
return "1"
88+
elif key == "GETDEPS_WGET_ARGS":
89+
return ""
90+
else:
91+
return None
92+
93+
mock_random.return_value = 0
94+
95+
mock_run.side_effect = [
96+
IOError("<urlopen error [Errno 104] Connection reset by peer>"),
97+
IOError("<urlopen error [Errno 104] Connection reset by peer>"),
98+
None,
99+
]
100+
mock_makedirs.side_effect = custom_makedirs
101+
mock_os_environ_get.side_effect = custom_get
102+
mock_sleep.side_effect = None
103+
fetcher = self._get_archive_fetcher()
104+
fetcher._verify_hash = MagicMock(return_value=None)
105+
fetcher._download()
106+
mock_sleep.assert_has_calls([call(2), call(4)], any_order=False)
107+
calls = [
108+
call(
109+
[
110+
"wget",
111+
"-O",
112+
"/path/to/scratch_dir/downloads/mock_manifest_parser-v256.7.tar.gz",
113+
"https://github.com/systemd/systemd/archive/refs/tags/v256.7.tar.gz",
114+
],
115+
capture_output=True,
116+
),
117+
] * 3
118+
119+
mock_run.assert_has_calls(calls, any_order=False)
120+
121+
@patch("random.random")
122+
@patch("os.makedirs")
123+
@patch("os.environ.get")
124+
@patch("time.sleep")
125+
@patch("subprocess.run")
126+
def test_all_retries(
127+
self, mock_run, mock_sleep, mock_os_environ_get, mock_makedirs, mock_random
128+
) -> None:
129+
def custom_makedirs(path, exist_ok=False):
130+
return None
131+
132+
def custom_get(key, default=None):
133+
if key == "GETDEPS_USE_WGET":
134+
return "1"
135+
elif key == "GETDEPS_WGET_ARGS":
136+
return ""
137+
else:
138+
return None
139+
140+
mock_random.return_value = 0
141+
142+
mock_run.side_effect = IOError(
143+
"<urlopen error [Errno 104] Connection reset by peer>"
144+
)
145+
mock_makedirs.side_effect = custom_makedirs
146+
mock_os_environ_get.side_effect = custom_get
147+
mock_sleep.side_effect = None
148+
fetcher = self._get_archive_fetcher()
149+
fetcher._verify_hash = MagicMock(return_value=None)
150+
with self.assertRaises(TransientFailure):
151+
fetcher._download()
152+
mock_sleep.assert_has_calls(
153+
[call(2), call(4), call(8), call(10)], any_order=False
154+
)
155+
calls = [
156+
call(
157+
[
158+
"wget",
159+
"-O",
160+
"/path/to/scratch_dir/downloads/mock_manifest_parser-v256.7.tar.gz",
161+
"https://github.com/systemd/systemd/archive/refs/tags/v256.7.tar.gz",
162+
],
163+
capture_output=True,
164+
),
165+
] * 5
166+
167+
mock_run.assert_has_calls(calls, any_order=False)

0 commit comments

Comments
 (0)