Add optional support for retrying certain HTTP codes
Ironic commonly returns HTTP 409 when a node is locked by another routine and HTTP 503 when the conductor has no free threads to process the request. Currently it is managed by custom code in ironicclient and openstacksdk, this change will allow to move it to Session itself. Change-Id: I04e356e7856b020cd20aa598e291ef31e02730d2
This commit is contained in:
parent
db5aa8b3ae
commit
3c2cf44e1c
@ -80,6 +80,14 @@ class Adapter(object):
|
||||
a per-request feature, a user may know they
|
||||
want to default to sending a specific value.
|
||||
(optional)
|
||||
:param int status_code_retries: the maximum number of retries that
|
||||
should be attempted for retriable
|
||||
HTTP status codes (optional, defaults
|
||||
to 0 - never retry).
|
||||
:param list retriable_status_codes: list of HTTP status codes that
|
||||
should be retried (optional,
|
||||
defaults to HTTP 503, has no effect
|
||||
when status_code_retries is 0).
|
||||
"""
|
||||
|
||||
client_name = None
|
||||
@ -93,7 +101,8 @@ class Adapter(object):
|
||||
client_version=None, allow_version_hack=None,
|
||||
global_request_id=None,
|
||||
min_version=None, max_version=None,
|
||||
default_microversion=None):
|
||||
default_microversion=None, status_code_retries=None,
|
||||
retriable_status_codes=None):
|
||||
if version and (min_version or max_version):
|
||||
raise TypeError(
|
||||
"version is mutually exclusive with min_version and"
|
||||
@ -120,6 +129,8 @@ class Adapter(object):
|
||||
self.min_version = min_version
|
||||
self.max_version = max_version
|
||||
self.default_microversion = default_microversion
|
||||
self.status_code_retries = status_code_retries
|
||||
self.retriable_status_codes = retriable_status_codes
|
||||
|
||||
self.global_request_id = global_request_id
|
||||
|
||||
@ -159,6 +170,11 @@ class Adapter(object):
|
||||
kwargs.setdefault('user_agent', self.user_agent)
|
||||
if self.connect_retries is not None:
|
||||
kwargs.setdefault('connect_retries', self.connect_retries)
|
||||
if self.status_code_retries is not None:
|
||||
kwargs.setdefault('status_code_retries', self.status_code_retries)
|
||||
if self.retriable_status_codes:
|
||||
kwargs.setdefault('retriable_status_codes',
|
||||
self.retriable_status_codes)
|
||||
if self.logger:
|
||||
kwargs.setdefault('logger', self.logger)
|
||||
if self.allow:
|
||||
|
@ -560,6 +560,7 @@ class Session(object):
|
||||
endpoint_override=None, connect_retries=0, logger=None,
|
||||
allow=None, client_name=None, client_version=None,
|
||||
microversion=None, microversion_service_type=None,
|
||||
status_code_retries=0, retriable_status_codes=None,
|
||||
**kwargs):
|
||||
"""Send an HTTP request with the specified characteristics.
|
||||
|
||||
@ -638,6 +639,14 @@ class Session(object):
|
||||
provided or does not have a service_type, microversion
|
||||
is given and microversion_service_type is not provided,
|
||||
an exception will be raised.
|
||||
:param int status_code_retries: the maximum number of retries that
|
||||
should be attempted for retriable
|
||||
HTTP status codes (optional, defaults
|
||||
to 0 - never retry).
|
||||
:param list retriable_status_codes: list of HTTP status codes that
|
||||
should be retried (optional,
|
||||
defaults to HTTP 503, has no effect
|
||||
when status_code_retries is 0).
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
:meth:`requests.Session.request` (such as `headers`).
|
||||
Except:
|
||||
@ -659,6 +668,8 @@ class Session(object):
|
||||
else:
|
||||
split_loggers = None
|
||||
logger = logger or utils.get_logger(__name__)
|
||||
# HTTP 503 - Service Unavailable
|
||||
retriable_status_codes = retriable_status_codes or [503]
|
||||
|
||||
headers = kwargs.setdefault('headers', dict())
|
||||
if microversion:
|
||||
@ -785,7 +796,8 @@ class Session(object):
|
||||
|
||||
send = functools.partial(self._send_request,
|
||||
url, method, redirect, log, logger,
|
||||
split_loggers, connect_retries)
|
||||
split_loggers, connect_retries,
|
||||
status_code_retries, retriable_status_codes)
|
||||
|
||||
try:
|
||||
connection_params = self.get_auth_connection_params(auth=auth)
|
||||
@ -872,7 +884,9 @@ class Session(object):
|
||||
return resp
|
||||
|
||||
def _send_request(self, url, method, redirect, log, logger, split_loggers,
|
||||
connect_retries, connect_retry_delay=0.5, **kwargs):
|
||||
connect_retries, status_code_retries,
|
||||
retriable_status_codes, connect_retry_delay=0.5,
|
||||
status_code_retry_delay=0.5, **kwargs):
|
||||
# NOTE(jamielennox): We handle redirection manually because the
|
||||
# requests lib follows some browser patterns where it will redirect
|
||||
# POSTs as GETs for certain statuses which is not want we want for an
|
||||
@ -918,6 +932,8 @@ class Session(object):
|
||||
|
||||
return self._send_request(
|
||||
url, method, redirect, log, logger, split_loggers,
|
||||
status_code_retries=status_code_retries,
|
||||
retriable_status_codes=retriable_status_codes,
|
||||
connect_retries=connect_retries - 1,
|
||||
connect_retry_delay=connect_retry_delay * 2,
|
||||
**kwargs)
|
||||
@ -949,12 +965,32 @@ class Session(object):
|
||||
new_resp = self._send_request(
|
||||
location, method, redirect, log, logger, split_loggers,
|
||||
connect_retries=connect_retries,
|
||||
status_code_retries=status_code_retries,
|
||||
retriable_status_codes=retriable_status_codes,
|
||||
**kwargs)
|
||||
|
||||
if not isinstance(new_resp.history, list):
|
||||
new_resp.history = list(new_resp.history)
|
||||
new_resp.history.insert(0, resp)
|
||||
resp = new_resp
|
||||
elif (resp.status_code in retriable_status_codes and
|
||||
status_code_retries > 0):
|
||||
|
||||
logger.info('Retriable status code %(code)s. Retrying in '
|
||||
'%(delay).1fs.',
|
||||
{'code': resp.status_code,
|
||||
'delay': status_code_retry_delay})
|
||||
time.sleep(status_code_retry_delay)
|
||||
|
||||
# NOTE(jamielennox): We don't pass through connect_retry_delay.
|
||||
# This request actually worked so we can reset the delay count.
|
||||
return self._send_request(
|
||||
url, method, redirect, log, logger, split_loggers,
|
||||
connect_retries=connect_retries,
|
||||
status_code_retries=status_code_retries - 1,
|
||||
retriable_status_codes=retriable_status_codes,
|
||||
status_code_retry_delay=status_code_retry_delay * 2,
|
||||
**kwargs)
|
||||
|
||||
return resp
|
||||
|
||||
|
@ -440,6 +440,62 @@ class SessionTests(utils.TestCase):
|
||||
self.assertThat(self.requests_mock.request_history,
|
||||
matchers.HasLength(retries + 1))
|
||||
|
||||
def test_http_503_retries(self):
|
||||
self.stub_url('GET', status_code=503)
|
||||
|
||||
session = client_session.Session()
|
||||
retries = 3
|
||||
|
||||
with mock.patch('time.sleep') as m:
|
||||
self.assertRaises(exceptions.ServiceUnavailable,
|
||||
session.get,
|
||||
self.TEST_URL, status_code_retries=retries)
|
||||
|
||||
self.assertEqual(retries, m.call_count)
|
||||
# 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0
|
||||
m.assert_called_with(2.0)
|
||||
|
||||
# we count retries so there will be one initial request + 3 retries
|
||||
self.assertThat(self.requests_mock.request_history,
|
||||
matchers.HasLength(retries + 1))
|
||||
|
||||
def test_http_status_retries(self):
|
||||
self.stub_url('GET', status_code=409)
|
||||
|
||||
session = client_session.Session()
|
||||
retries = 3
|
||||
|
||||
with mock.patch('time.sleep') as m:
|
||||
self.assertRaises(exceptions.Conflict,
|
||||
session.get,
|
||||
self.TEST_URL, status_code_retries=retries,
|
||||
retriable_status_codes=[503, 409])
|
||||
|
||||
self.assertEqual(retries, m.call_count)
|
||||
# 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0
|
||||
m.assert_called_with(2.0)
|
||||
|
||||
# we count retries so there will be one initial request + 3 retries
|
||||
self.assertThat(self.requests_mock.request_history,
|
||||
matchers.HasLength(retries + 1))
|
||||
|
||||
def test_http_status_retries_another_code(self):
|
||||
self.stub_url('GET', status_code=404)
|
||||
|
||||
session = client_session.Session()
|
||||
retries = 3
|
||||
|
||||
with mock.patch('time.sleep') as m:
|
||||
self.assertRaises(exceptions.NotFound,
|
||||
session.get,
|
||||
self.TEST_URL, status_code_retries=retries,
|
||||
retriable_status_codes=[503, 409])
|
||||
|
||||
self.assertFalse(m.called)
|
||||
|
||||
self.assertThat(self.requests_mock.request_history,
|
||||
matchers.HasLength(1))
|
||||
|
||||
def test_uses_tcp_keepalive_by_default(self):
|
||||
session = client_session.Session()
|
||||
requests_session = session.session
|
||||
@ -1217,6 +1273,39 @@ class AdapterTest(utils.TestCase):
|
||||
self.assertThat(self.requests_mock.request_history,
|
||||
matchers.HasLength(retries + 1))
|
||||
|
||||
def test_adapter_http_503_retries(self):
|
||||
retries = 2
|
||||
sess = client_session.Session()
|
||||
adpt = adapter.Adapter(sess, status_code_retries=retries)
|
||||
|
||||
self.stub_url('GET', status_code=503)
|
||||
|
||||
with mock.patch('time.sleep') as m:
|
||||
self.assertRaises(exceptions.ServiceUnavailable,
|
||||
adpt.get, self.TEST_URL)
|
||||
self.assertEqual(retries, m.call_count)
|
||||
|
||||
# we count retries so there will be one initial request + 2 retries
|
||||
self.assertThat(self.requests_mock.request_history,
|
||||
matchers.HasLength(retries + 1))
|
||||
|
||||
def test_adapter_http_status_retries(self):
|
||||
retries = 2
|
||||
sess = client_session.Session()
|
||||
adpt = adapter.Adapter(sess, status_code_retries=retries,
|
||||
retriable_status_codes=[503, 409])
|
||||
|
||||
self.stub_url('GET', status_code=409)
|
||||
|
||||
with mock.patch('time.sleep') as m:
|
||||
self.assertRaises(exceptions.Conflict,
|
||||
adpt.get, self.TEST_URL)
|
||||
self.assertEqual(retries, m.call_count)
|
||||
|
||||
# we count retries so there will be one initial request + 2 retries
|
||||
self.assertThat(self.requests_mock.request_history,
|
||||
matchers.HasLength(retries + 1))
|
||||
|
||||
def test_user_and_project_id(self):
|
||||
auth = AuthPlugin()
|
||||
sess = client_session.Session()
|
||||
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Addes support for retrying certain HTTP status codes when doing requests
|
||||
via the new ``status_code_retries`` and ``retriable_status_codes``
|
||||
parameters for ``Session`` and ``Adapter``.
|
Loading…
Reference in New Issue
Block a user