NEC plugin: Honor Retry-After response from OFC

A backend OpenFlow controller nec plugin talks to can return
503 response with retry-after header when it is busy.
It is better to honor retry-after header to avoid unnecessary
user-visible errors due to temporary busy condition.

Change-Id: I2ff1c3ac8402a2207bd955e9a9bb61e147950c5c
Closes-Bug: #1294527
This commit is contained in:
Akihiro Motoki 2014-03-19 17:08:37 +09:00
parent e75f4854a6
commit a79aa79b2a
5 changed files with 101 additions and 1 deletions

View File

@ -37,6 +37,11 @@ firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewal
# Certificate file # Certificate file
# cert_file = # cert_file =
# Maximum attempts per OFC API request. NEC plugin retries
# API request to OFC when OFC returns ServiceUnavailable (503).
# The value must be greater than 0.
# api_max_attempts = 3
[provider] [provider]
# Default router provider to use. # Default router provider to use.
# default_router_provider = l3-agent # default_router_provider = l3-agent

View File

@ -48,6 +48,11 @@ ofc_opts = [
help=_("Key file")), help=_("Key file")),
cfg.StrOpt('cert_file', default=None, cfg.StrOpt('cert_file', default=None,
help=_("Certificate file")), help=_("Certificate file")),
cfg.IntOpt('api_max_attempts', default=3,
help=_("Maximum attempts per OFC API request."
"NEC plugin retries API request to OFC "
"when OFC returns ServiceUnavailable (503)."
"The value must be greater than 0.")),
] ]
provider_opts = [ provider_opts = [

View File

@ -42,6 +42,15 @@ class OFCMappingNotFound(qexc.NotFound):
"It may be deleted during processing.") "It may be deleted during processing.")
class OFCServiceUnavailable(OFCException):
message = _("OFC returns Server Unavailable (503) "
"(Retry-After=%(retry_after)s)")
def __init__(self, **kwargs):
super(OFCServiceUnavailable, self).__init__(**kwargs)
self.retry_after = kwargs.get('retry_after')
class PortInfoNotFound(qexc.NotFound): class PortInfoNotFound(qexc.NotFound):
message = _("PortInfo %(id)s could not be found") message = _("PortInfo %(id)s could not be found")

View File

@ -18,8 +18,10 @@
import httplib import httplib
import json import json
import socket import socket
import time
from neutron.openstack.common import log as logging from neutron.openstack.common import log as logging
from neutron.plugins.nec.common import config
from neutron.plugins.nec.common import exceptions as nexc from neutron.plugins.nec.common import exceptions as nexc
@ -67,7 +69,7 @@ class OFCClient(object):
return (_("Operation on OFC failed: %(status)s%(msg)s") % return (_("Operation on OFC failed: %(status)s%(msg)s") %
{'status': status, 'msg': detail}) {'status': status, 'msg': detail})
def do_request(self, method, action, body=None): def do_single_request(self, method, action, body=None):
LOG.debug(_("Client request: %(host)s:%(port)s " LOG.debug(_("Client request: %(host)s:%(port)s "
"%(method)s %(action)s [%(body)s]"), "%(method)s %(action)s [%(body)s]"),
{'host': self.host, 'port': self.port, {'host': self.host, 'port': self.port,
@ -95,6 +97,11 @@ class OFCClient(object):
httplib.ACCEPTED, httplib.ACCEPTED,
httplib.NO_CONTENT): httplib.NO_CONTENT):
return data return data
elif res.status == httplib.SERVICE_UNAVAILABLE:
retry_after = res.getheader('retry-after')
LOG.warning(_("OFC returns ServiceUnavailable "
"(retry-after=%s)"), retry_after)
raise nexc.OFCServiceUnavailable(retry_after=retry_after)
elif res.status == httplib.NOT_FOUND: elif res.status == httplib.NOT_FOUND:
LOG.info(_("Specified resource %s does not exist on OFC "), LOG.info(_("Specified resource %s does not exist on OFC "),
action) action)
@ -116,6 +123,23 @@ class OFCClient(object):
LOG.error(reason) LOG.error(reason)
raise nexc.OFCException(reason=reason) raise nexc.OFCException(reason=reason)
def do_request(self, method, action, body=None):
max_attempts = config.OFC.api_max_attempts
for i in range(max_attempts, 0, -1):
try:
return self.do_single_request(method, action, body)
except nexc.OFCServiceUnavailable as e:
try:
wait_time = int(e.retry_after)
except (ValueError, TypeError):
wait_time = None
if i > 1 and wait_time:
LOG.info(_("Waiting for %s seconds due to "
"OFC Service_Unavailable."), wait_time)
time.sleep(wait_time)
continue
raise
def get(self, action): def get(self, action):
return self.do_request("GET", action) return self.do_request("GET", action)

View File

@ -20,6 +20,7 @@ import json
import socket import socket
import mock import mock
from oslo.config import cfg
from neutron.plugins.nec.common import exceptions as nexc from neutron.plugins.nec.common import exceptions as nexc
from neutron.plugins.nec.common import ofc_client from neutron.plugins.nec.common import ofc_client
@ -120,3 +121,59 @@ class OFCClientTest(base.BaseTestCase):
mock.call.request('GET', '/somewhere', '{}', headers), mock.call.request('GET', '/somewhere', '{}', headers),
] ]
conn.assert_has_calls(expected) conn.assert_has_calls(expected)
def test_do_request_retry_fail_after_one_attempts(self):
self._test_do_request_retry_after(1, api_max_attempts=1)
def test_do_request_retry_fail_with_max_attempts(self):
self._test_do_request_retry_after(3)
def test_do_request_retry_succeed_with_2nd_attempt(self):
self._test_do_request_retry_after(2, succeed_final=True)
def test_do_request_retry_succeed_with_1st_attempt(self):
self._test_do_request_retry_after(1, succeed_final=True)
def _test_do_request_retry_after(self, exp_request_count,
api_max_attempts=None,
succeed_final=False):
if api_max_attempts is not None:
cfg.CONF.set_override('api_max_attempts', api_max_attempts,
group='OFC')
res_unavail = mock.Mock()
res_unavail.status = 503
res_unavail.read.return_value = None
res_unavail.getheader.return_value = '10'
res_ok = mock.Mock()
res_ok.status = 200
res_ok.read.return_value = None
conn = mock.Mock()
if succeed_final:
side_effect = [res_unavail] * (exp_request_count - 1) + [res_ok]
else:
side_effect = [res_unavail] * exp_request_count
conn.getresponse.side_effect = side_effect
with mock.patch.object(ofc_client.OFCClient, 'get_connection',
return_value=conn):
with mock.patch('time.sleep') as sleep:
client = ofc_client.OFCClient()
if succeed_final:
ret = client.do_request('GET', '/somewhere')
self.assertIsNone(ret)
else:
e = self.assertRaises(nexc.OFCServiceUnavailable,
client.do_request,
'GET', '/somewhere')
self.assertEqual('10', e.retry_after)
headers = {"Content-Type": "application/json"}
expected = [
mock.call.request('GET', '/somewhere', None, headers),
mock.call.getresponse(),
] * exp_request_count
conn.assert_has_calls(expected)
self.assertEqual(exp_request_count, conn.request.call_count)
self.assertEqual(exp_request_count - 1, sleep.call_count)