From 7f4f083a48e97ff37192caf5bb292c45b0788c92 Mon Sep 17 00:00:00 2001 From: "Luis A. Garcia" Date: Tue, 5 Nov 2013 19:37:27 +0000 Subject: [PATCH] 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 (cherry picked from commit 233430b51ddbd207f22eee98386ffbb766fd777f) --- cinder/api/openstack/wsgi.py | 74 +++++++++++++++++++++++++ cinder/tests/api/openstack/test_wsgi.py | 34 ++++++++++++ 2 files changed, 108 insertions(+) diff --git a/cinder/api/openstack/wsgi.py b/cinder/api/openstack/wsgi.py index 99ba8e49094..77ac41715e9 100644 --- a/cinder/api/openstack/wsgi.py +++ b/cinder/api/openstack/wsgi.py @@ -65,6 +65,80 @@ 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 bce238d45d4..3e00810ee78 100644 --- a/cinder/tests/api/openstack/test_wsgi.py +++ b/cinder/tests/api/openstack/test_wsgi.py @@ -107,6 +107,40 @@ def fake_best_match(self, offers, default_match=None): request.headers.pop('Accept-Language') self.assertEqual(request.best_match_language(), None) + 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):