Merge "Allow initializing session with connection retries"

This commit is contained in:
Zuul 2019-10-09 20:23:21 +00:00 committed by Gerrit Code Review
commit 5e5185f80f
3 changed files with 69 additions and 9 deletions

View File

@ -330,6 +330,9 @@ class Session(object):
:param rate_semaphore: Semaphore to be used to control concurrency :param rate_semaphore: Semaphore to be used to control concurrency
and rate limiting of requests. (optional, and rate limiting of requests. (optional,
defaults to no concurrency or rate control) defaults to no concurrency or rate control)
:param int connect_retries: the maximum number of retries that should
be attempted for connection errors.
(optional, defaults to 0 - never retry).
""" """
user_agent = None user_agent = None
@ -343,7 +346,8 @@ class Session(object):
redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None, redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None,
app_name=None, app_version=None, additional_user_agent=None, app_name=None, app_version=None, additional_user_agent=None,
discovery_cache=None, split_loggers=None, discovery_cache=None, split_loggers=None,
collect_timing=False, rate_semaphore=None): collect_timing=False, rate_semaphore=None,
connect_retries=0):
self.auth = auth self.auth = auth
self.session = _construct_session(session) self.session = _construct_session(session)
@ -371,6 +375,7 @@ class Session(object):
# so we can distinguish between the value being set or not. # so we can distinguish between the value being set or not.
self._split_loggers = split_loggers self._split_loggers = split_loggers
self._collect_timing = collect_timing self._collect_timing = collect_timing
self._connect_retries = connect_retries
self._api_times = [] self._api_times = []
self._rate_semaphore = rate_semaphore or NoOpSemaphore() self._rate_semaphore = rate_semaphore or NoOpSemaphore()
@ -621,7 +626,7 @@ class Session(object):
user_agent=None, redirect=None, authenticated=None, user_agent=None, redirect=None, authenticated=None,
endpoint_filter=None, auth=None, requests_auth=None, endpoint_filter=None, auth=None, requests_auth=None,
raise_exc=True, allow_reauth=True, log=True, raise_exc=True, allow_reauth=True, log=True,
endpoint_override=None, connect_retries=0, logger=None, endpoint_override=None, connect_retries=None, logger=None,
allow=None, client_name=None, client_version=None, allow=None, client_name=None, client_version=None,
microversion=None, microversion_service_type=None, microversion=None, microversion_service_type=None,
status_code_retries=0, retriable_status_codes=None, status_code_retries=0, retriable_status_codes=None,
@ -654,7 +659,7 @@ class Session(object):
for forever/never. (optional) for forever/never. (optional)
:param int connect_retries: the maximum number of retries that should :param int connect_retries: the maximum number of retries that should
be attempted for connection errors. be attempted for connection errors.
(optional, defaults to 0 - never retry). (optional, defaults to None - never retry).
:param bool authenticated: True if a token should be attached to this :param bool authenticated: True if a token should be attached to this
request, False if not or None for attach if request, False if not or None for attach if
an auth_plugin is available. an auth_plugin is available.
@ -748,6 +753,8 @@ class Session(object):
else: else:
split_loggers = None split_loggers = None
logger = logger or utils.get_logger(__name__) logger = logger or utils.get_logger(__name__)
if connect_retries is None:
connect_retries = self._connect_retries
# HTTP 503 - Service Unavailable # HTTP 503 - Service Unavailable
retriable_status_codes = retriable_status_codes or [503] retriable_status_codes = retriable_status_codes or [503]
rate_semaphore = rate_semaphore or self._rate_semaphore rate_semaphore = rate_semaphore or self._rate_semaphore

View File

@ -421,24 +421,66 @@ class SessionTests(utils.TestCase):
self.assertIn('--cacert', self.logger.output) self.assertIn('--cacert', self.logger.output)
self.assertIn(path_to_certs, self.logger.output) self.assertIn(path_to_certs, self.logger.output)
def test_connect_retries(self): def _connect_retries_check(self, session, expected_retries=0,
call_args=None):
call_args = call_args or {}
self.stub_url('GET', exc=requests.exceptions.Timeout()) self.stub_url('GET', exc=requests.exceptions.Timeout())
session = client_session.Session() call_args['url'] = self.TEST_URL
retries = 3
with mock.patch('time.sleep') as m: with mock.patch('time.sleep') as m:
self.assertRaises(exceptions.ConnectTimeout, self.assertRaises(exceptions.ConnectTimeout,
session.get, session.get,
self.TEST_URL, connect_retries=retries) **call_args)
self.assertEqual(retries, m.call_count) self.assertEqual(expected_retries, m.call_count)
# 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0 # 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0
m.assert_called_with(2.0) m.assert_called_with(2.0)
# we count retries so there will be one initial request + 3 retries # we count retries so there will be one initial request + 3 retries
self.assertThat(self.requests_mock.request_history, self.assertThat(self.requests_mock.request_history,
matchers.HasLength(retries + 1)) matchers.HasLength(expected_retries + 1))
def test_session_connect_retries(self):
retries = 3
session = client_session.Session(connect_retries=retries)
self._connect_retries_check(session=session, expected_retries=retries)
def test_call_args_connect_retries_session_init(self):
session = client_session.Session()
retries = 3
call_args = {'connect_retries': retries}
self._connect_retries_check(session=session,
expected_retries=retries,
call_args=call_args)
def test_call_args_connect_retries_overrides_session_retries(self):
session_retries = 6
call_arg_retries = 3
call_args = {'connect_retries': call_arg_retries}
session = client_session.Session(connect_retries=session_retries)
self._connect_retries_check(session=session,
expected_retries=call_arg_retries,
call_args=call_args)
def test_override_session_connect_retries_for_request(self):
session_retries = 1
session = client_session.Session(connect_retries=session_retries)
self.stub_url('GET', exc=requests.exceptions.Timeout())
call_args = {'connect_retries': 0}
with mock.patch('time.sleep') as m:
self.assertRaises(
exceptions.ConnectTimeout,
session.request,
self.TEST_URL,
'GET',
**call_args
)
self.assertEqual(0, m.call_count)
def test_connect_retries_interval_limit(self): def test_connect_retries_interval_limit(self):
self.stub_url('GET', exc=requests.exceptions.Timeout()) self.stub_url('GET', exc=requests.exceptions.Timeout())

View File

@ -0,0 +1,11 @@
---
features:
- |
[`feature bug 1840235 <https://bugs.launchpad.net/keystoneauth/+bug/1840235>`_]
Adds ``connect_retries`` to Session.__init__(), that can then be used
by projects when creating session objects, to set the required number of
retries for new connection requests. This would specifically help avoid
a scalability issue that results in number of ConnectTimeout errors when
doing endpoint discovery and fetching roles using an auth plugin under
heavy load. This still allows for it to be overridden per service with
the adapter interface.