From: Luis A. Garcia Date: Tue, 5 Nov 2013 19:37:27 +0000 (+0000) Subject: Enable object caching in cinder REST API requests X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=233430b51ddbd207f22eee98386ffbb766fd777f;p=openstack-build%2Fcinder-build.git Enable object caching in cinder REST API requests Allow the core API to cache resources, such as DB results, so that extensions can use data already retrieved within the same API request eliminating additional expensive DB calls. Loosely based on commit 9f9fbc54e7336da10fc3056bdaca2ec7d01c7f94 from nova. Change-Id: If9f49faf7305287c0489ad6209cf19b8bec612cc Partial-Bug: #1197612 --- diff --git a/cinder/api/openstack/wsgi.py b/cinder/api/openstack/wsgi.py index 64a5fecb2..d2eee19cf 100644 --- a/cinder/api/openstack/wsgi.py +++ b/cinder/api/openstack/wsgi.py @@ -66,6 +66,80 @@ _MEDIA_TYPE_MAP = { class Request(webob.Request): """Add some OpenStack API-specific logic to the base webob.Request.""" + def __init__(self, *args, **kwargs): + super(Request, self).__init__(*args, **kwargs) + self._resource_cache = {} + + def cache_resource(self, resource_to_cache, id_attribute='id', name=None): + """Cache the given resource. + + Allow API methods to cache objects, such as results from a DB query, + to be used by API extensions within the same API request. + + The resource_to_cache can be a list or an individual resource, + but ultimately resources are cached individually using the given + id_attribute. + + Different resources types might need to be cached during the same + request, they can be cached using the name parameter. For example: + + Controller 1: + request.cache_resource(db_volumes, 'volumes') + request.cache_resource(db_volume_types, 'types') + Controller 2: + db_volumes = request.cached_resource('volumes') + db_type_1 = request.cached_resource_by_id('1', 'types') + + If no name is given, a default name will be used for the resource. + + An instance of this class only lives for the lifetime of a + single API request, so there's no need to implement full + cache management. + """ + if not isinstance(resource_to_cache, list): + resource_to_cache = [resource_to_cache] + if not name: + name = self.path + cached_resources = self._resource_cache.setdefault(name, {}) + for resource in resource_to_cache: + cached_resources[resource[id_attribute]] = resource + + def cached_resource(self, name=None): + """Get the cached resources cached under the given resource name. + + Allow an API extension to get previously stored objects within + the same API request. + + Note that the object data will be slightly stale. + + :returns: a dict of id_attribute to the resource from the cached + resources, an empty map if an empty collection was cached, + or None if nothing has been cached yet under this name + """ + if not name: + name = self.path + if name not in self._resource_cache: + # Nothing has been cached for this key yet + return None + return self._resource_cache[name] + + def cached_resource_by_id(self, resource_id, name=None): + """Get a resource by ID cached under the given resource name. + + Allow an API extension to get a previously stored object + within the same API request. This is basically a convenience method + to lookup by ID on the dictionary of all cached resources. + + Note that the object data will be slightly stale. + + :returns: the cached resource or None if the item is not in the cache + """ + resources = self.cached_resource(name) + if not resources: + # Nothing has been cached yet for this key yet + return None + return resources.get(resource_id) + def best_match_content_type(self): """Determine the requested response content-type.""" if 'cinder.best_content_type' not in self.environ: diff --git a/cinder/tests/api/openstack/test_wsgi.py b/cinder/tests/api/openstack/test_wsgi.py index 892adf6c4..259911e57 100644 --- a/cinder/tests/api/openstack/test_wsgi.py +++ b/cinder/tests/api/openstack/test_wsgi.py @@ -107,6 +107,40 @@ class RequestTest(test.TestCase): request.headers.pop('Accept-Language') self.assertIsNone(request.best_match_language()) + def test_cache_and_retrieve_resources(self): + request = wsgi.Request.blank('/foo') + # Test that trying to retrieve a cached object on + # an empty cache fails gracefully + self.assertIsNone(request.cached_resource()) + self.assertIsNone(request.cached_resource_by_id('r-0')) + + resources = [] + for x in xrange(3): + resources.append({'id': 'r-%s' % x}) + + # Cache an empty list of resources using the default name + request.cache_resource([]) + self.assertEqual({}, request.cached_resource()) + self.assertIsNone(request.cached_resource('r-0')) + # Cache some resources + request.cache_resource(resources[:2]) + # Cache one resource + request.cache_resource(resources[2]) + # Cache a different resource name + other_resource = {'id': 'o-0'} + request.cache_resource(other_resource, name='other-resource') + + self.assertEqual(resources[0], request.cached_resource_by_id('r-0')) + self.assertEqual(resources[1], request.cached_resource_by_id('r-1')) + self.assertEqual(resources[2], request.cached_resource_by_id('r-2')) + self.assertIsNone(request.cached_resource_by_id('r-3')) + self.assertEqual({'r-0': resources[0], + 'r-1': resources[1], + 'r-2': resources[2]}, request.cached_resource()) + self.assertEqual(other_resource, + request.cached_resource_by_id('o-0', + name='other-resource')) + class ActionDispatcherTest(test.TestCase): def test_dispatch(self):