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:
Kevin Benton 2016-03-14 07:33:04 -07:00
parent 19348ecd6e
commit 1f017d349d
2 changed files with 76 additions and 6 deletions
neutron
pecan_wsgi/controllers
tests/functional/pecan_wsgi

@ -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'}},