]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Enable object caching in cinder REST API requests
authorLuis A. Garcia <luis@linux.vnet.ibm.com>
Tue, 5 Nov 2013 19:37:27 +0000 (19:37 +0000)
committerLuis A. Garcia <luis@linux.vnet.ibm.com>
Wed, 13 Nov 2013 00:23:51 +0000 (00:23 +0000)
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

cinder/api/openstack/wsgi.py
cinder/tests/api/openstack/test_wsgi.py

index 64a5fecb27220d62182b48e2d3f2005866fda510..d2eee19cfe8f5c5891fb30b4fff8b2e9156235a3 100644 (file)
@@ -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:
index 892adf6c46d6ebb71551e53d90f875c70dae6d47..259911e57e3c2f295375cf115f64c58e48a84d91 100644 (file)
@@ -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):