Add retry support to pecan
This modifies the expose and when decorators in pecan to also include the olso db API retry logic so all controllers that leverage these automatically get the retry logic to handle temporary DB errors. To accomplish this, the ATTR_NOT_SPECIFIED sentinel was modified to be friendly to copy.deepcopy since unlike in the old API, the new logic will be sending copies to the core plugin after the attributes have been populated. This also includes a workaround for a bad assumption in the pecan decorator utils that assumes all decorators have a __code__ attribute. Change-Id: If4e502d25856190495b160a589232de258364c4f Closes-Bug: #1557509 Co-Authored-By: Brandon Logan <brandon.logan@rackspace.com>
This commit is contained in:
parent
19348ecd6e
commit
1f017d349d
neutron
@ -13,27 +13,76 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import functools
|
||||
|
||||
from neutron_lib import constants
|
||||
import pecan
|
||||
from pecan import request
|
||||
|
||||
from neutron.api.v2 import attributes as api_attributes
|
||||
from neutron.db import api as db_api
|
||||
from neutron import manager
|
||||
|
||||
# Utility functions for Pecan controllers.
|
||||
|
||||
|
||||
def expose(*args, **kwargs):
|
||||
class Fakecode(object):
|
||||
co_varnames = ()
|
||||
|
||||
|
||||
def _composed(*decorators):
|
||||
"""Takes a list of decorators and returns a single decorator."""
|
||||
|
||||
def final_decorator(f):
|
||||
for d in decorators:
|
||||
# workaround for pecan bug that always assumes decorators
|
||||
# have a __code__ attr
|
||||
if not hasattr(d, '__code__'):
|
||||
setattr(d, '__code__', Fakecode())
|
||||
f = d(f)
|
||||
return f
|
||||
return final_decorator
|
||||
|
||||
|
||||
def _protect_original_resources(f):
|
||||
"""Wrapper to ensure that mutated resources are discarded on retries."""
|
||||
|
||||
@functools.wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
ctx = request.context
|
||||
if 'resources' in ctx:
|
||||
orig = ctx.get('protected_resources')
|
||||
if not orig:
|
||||
# this is the first call so we just take the whole reference
|
||||
ctx['protected_resources'] = ctx['resources']
|
||||
# TODO(blogan): Once bug 157751 is fixed and released in
|
||||
# neutron-lib this memo will no longer be needed. This is just
|
||||
# quick way to not depend on a release of neutron-lib.
|
||||
# The version that has that bug fix will need to be updated in
|
||||
# neutron-lib.
|
||||
memo = {id(constants.ATTR_NOT_SPECIFIED):
|
||||
constants.ATTR_NOT_SPECIFIED}
|
||||
ctx['resources'] = copy.deepcopy(ctx['protected_resources'],
|
||||
memo=memo)
|
||||
return f(*args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
def _pecan_generator_wrapper(func, *args, **kwargs):
|
||||
"""Helper function so we don't have to specify json for everything."""
|
||||
kwargs.setdefault('content_type', 'application/json')
|
||||
kwargs.setdefault('template', 'json')
|
||||
return pecan.expose(*args, **kwargs)
|
||||
return _composed(_protect_original_resources, db_api.retry_db_errors,
|
||||
func(*args, **kwargs))
|
||||
|
||||
|
||||
def expose(*args, **kwargs):
|
||||
return _pecan_generator_wrapper(pecan.expose, *args, **kwargs)
|
||||
|
||||
|
||||
def when(index, *args, **kwargs):
|
||||
"""Helper function so we don't have to specify json for everything."""
|
||||
kwargs.setdefault('content_type', 'application/json')
|
||||
kwargs.setdefault('template', 'json')
|
||||
return index.when(*args, **kwargs)
|
||||
return _pecan_generator_wrapper(index.when, *args, **kwargs)
|
||||
|
||||
|
||||
class NeutronPecanController(object):
|
||||
|
@ -15,6 +15,7 @@ from collections import namedtuple
|
||||
import mock
|
||||
from neutron_lib import constants as n_const
|
||||
from oslo_config import cfg
|
||||
from oslo_db import exception as db_exc
|
||||
from oslo_policy import policy as oslo_policy
|
||||
from oslo_serialization import jsonutils
|
||||
import pecan
|
||||
@ -268,6 +269,26 @@ class TestResourceController(TestRootController):
|
||||
headers={'X-Project-Id': 'tenid'})
|
||||
self.assertEqual(response.status_int, 201)
|
||||
|
||||
def test_post_with_retry(self):
|
||||
self._create_failed = False
|
||||
orig = self.plugin.create_port
|
||||
|
||||
def new_create(*args, **kwargs):
|
||||
if not self._create_failed:
|
||||
self._create_failed = True
|
||||
raise db_exc.RetryRequest(ValueError())
|
||||
return orig(*args, **kwargs)
|
||||
|
||||
with mock.patch.object(self.plugin, 'create_port',
|
||||
new=new_create):
|
||||
response = self.app.post_json(
|
||||
'/v2.0/ports.json',
|
||||
params={'port': {'network_id': self.port['network_id'],
|
||||
'admin_state_up': True,
|
||||
'tenant_id': 'tenid'}},
|
||||
headers={'X-Project-Id': 'tenid'})
|
||||
self.assertEqual(201, response.status_int)
|
||||
|
||||
def test_put(self):
|
||||
response = self.app.put_json('/v2.0/ports/%s.json' % self.port['id'],
|
||||
params={'port': {'name': 'test'}},
|
||||
|
Loading…
x
Reference in New Issue
Block a user