Skip to content

Commit 91f6ad0

Browse files
committed
Make _authenticated_urlopen retryable
Signed-off-by: Kyle Fazzari <[email protected]>
1 parent 92a3bdf commit 91f6ad0

File tree

2 files changed

+102
-27
lines changed

2 files changed

+102
-27
lines changed

test/test_base.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def __eq__(self, other):
213213

214214
build_opener_mock.assert_called_once_with(
215215
_HTTPBasicAuthHandlerMatcher(self))
216-
open_mock.assert_called_once_with(url, timeout=None)
216+
open_mock.assert_called_once_with(url, timeout=10)
217217

218218
@mock.patch('vcstool.clients.vcs_base.urlopen', autospec=True)
219219
@mock.patch('vcstool.clients.vcs_base.build_opener', autospec=True)
@@ -243,7 +243,71 @@ def __eq__(self, other):
243243
return True
244244

245245
urlopen_mock.assert_called_once_with(
246-
_RequestMatcher(self), timeout=None)
246+
_RequestMatcher(self), timeout=10)
247+
248+
@mock.patch('vcstool.clients.vcs_base.urlopen', autospec=True)
249+
def test_load_url_retries(self, urlopen_mock):
250+
urlopen_read_mock = urlopen_mock.return_value.read
251+
urlopen_mock.side_effect = [
252+
HTTPError(None, 503, 'test1', None, None),
253+
HTTPError(None, 503, 'test2', None, None),
254+
HTTPError(None, 503, 'test3', None, None),
255+
]
256+
257+
with self.assertRaisesRegex(HTTPError, 'test3'):
258+
vcs_base.load_url('example.com')
259+
260+
self.assertEqual(len(urlopen_mock.mock_calls), 3)
261+
urlopen_mock.assert_has_calls([
262+
mock.call('example.com', timeout=10),
263+
mock.call('example.com', timeout=10),
264+
mock.call('example.com', timeout=10),
265+
])
266+
self.assertFalse(urlopen_read_mock.mock_calls)
267+
268+
@mock.patch('vcstool.clients.vcs_base.urlopen', autospec=True)
269+
def test_load_url_retries_authenticated(self, urlopen_mock):
270+
urlopen_read_mock = urlopen_mock.return_value.read
271+
urlopen_mock.side_effect = [
272+
HTTPError(None, 401, 'test1', None, None),
273+
HTTPError(None, 503, 'test2', None, None),
274+
HTTPError(None, 503, 'test3', None, None),
275+
HTTPError(None, 503, 'test4', None, None),
276+
]
277+
278+
machine = 'example.com'
279+
_create_netrc_file(
280+
os.path.join(self.default_auth_dir, '.netrc'),
281+
textwrap.dedent('''\
282+
machine %s
283+
password password
284+
''' % machine))
285+
286+
url = 'https://%s/foo/bar' % machine
287+
288+
with self.assertRaisesRegex(HTTPError, 'test4'):
289+
vcs_base.load_url(url)
290+
291+
self.assertEqual(len(urlopen_mock.mock_calls), 4)
292+
293+
class _RequestMatcher(object):
294+
def __init__(self, test):
295+
self.test = test
296+
297+
def __eq__(self, other):
298+
self.test.assertEqual(other.get_full_url(), url)
299+
self.test.assertEqual(
300+
other.get_header('Private-token'), 'password')
301+
return True
302+
303+
urlopen_mock.assert_has_calls([
304+
mock.call(url, timeout=10),
305+
mock.call(_RequestMatcher(self), timeout=10),
306+
mock.call(_RequestMatcher(self), timeout=10),
307+
mock.call(_RequestMatcher(self), timeout=10),
308+
])
309+
self.assertFalse(urlopen_read_mock.mock_calls)
310+
247311

248312

249313
def _create_netrc_file(path, contents):

vcstool/clients/vcs_base.py

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import errno
2+
import functools
23
import glob
34
import logging
45
import netrc
@@ -112,26 +113,17 @@ def run_command(cmd, cwd, env=None):
112113
def load_url(url, retry=2, retry_period=1, timeout=10):
113114
fh = None
114115
try:
115-
fh = urlopen(url, timeout=timeout)
116+
fh = _retryable_urlopen(url, timeout=timeout)
116117
except HTTPError as e:
117-
e.msg += ' (%s)' % url
118118
if e.code in (401, 404):
119119
# Try again, but with authentication
120120
fh = _authenticated_urlopen(url, timeout=timeout)
121-
elif e.code == 503 and retry:
122-
time.sleep(retry_period)
123-
return load_url(
124-
url, retry=retry - 1, retry_period=retry_period,
125-
timeout=timeout)
126121
if fh is None:
122+
e.msg += ' (%s)' % url
127123
raise
128124
except URLError as e:
129-
if isinstance(e.reason, socket.timeout) and retry:
130-
time.sleep(retry_period)
131-
return load_url(
132-
url, retry=retry - 1, retry_period=retry_period,
133-
timeout=timeout)
134125
raise URLError(str(e) + ' (%s)' % url)
126+
135127
return fh.read()
136128

137129

@@ -140,33 +132,52 @@ def test_url(url, retry=2, retry_period=1, timeout=10):
140132
request.get_method = lambda: 'HEAD'
141133

142134
try:
143-
response = urlopen(request)
135+
response = _retryable_urlopen(request)
144136
except HTTPError as e:
145-
if e.code == 503 and retry:
146-
time.sleep(retry_period)
147-
return test_url(
148-
url, retry=retry - 1, retry_period=retry_period,
149-
timeout=timeout)
150137
e.msg += ' (%s)' % url
151138
raise
152139
except URLError as e:
153-
if isinstance(e.reason, socket.timeout) and retry:
154-
time.sleep(retry_period)
155-
return test_url(
156-
url, retry=retry - 1, retry_period=retry_period,
157-
timeout=timeout)
158140
raise URLError(str(e) + ' (%s)' % url)
159141
return response
160142

161143

162-
def _authenticated_urlopen(uri, timeout=None):
144+
def _urlopen_retry(f):
145+
@functools.wraps(f)
146+
def _retryable_function(url, retry=2, retry_period=1, timeout=10):
147+
retry += 1
148+
149+
while True:
150+
try:
151+
retry -= 1
152+
return f(url, timeout=timeout)
153+
except HTTPError as e:
154+
if e.code != 503 or retry <= 0:
155+
raise
156+
except URLError as e:
157+
if not isinstance(e.reason, socket.timeout) or retry <= 0:
158+
raise
159+
160+
if retry > 0:
161+
time.sleep(retry_period)
162+
else:
163+
break
164+
165+
return _retryable_function
166+
167+
168+
@_urlopen_retry
169+
def _retryable_urlopen(url, timeout=10):
170+
return urlopen(url, timeout=timeout)
171+
172+
173+
@_urlopen_retry
174+
def _authenticated_urlopen(uri, timeout=10):
163175
machine = urlparse(uri).netloc
164176
if not machine:
165177
return None
166178

167179
credentials = _credentials_for_machine(machine)
168180
if credentials is None:
169-
logger.warning('No credentials found for "%s"' % machine)
170181
return None
171182

172183
(username, account, password) = credentials

0 commit comments

Comments
 (0)