Skip to content

Commit

Permalink
Enable object caching in cinder REST API requests
Browse files Browse the repository at this point in the history
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 233430b)
  • Loading branch information
Luis A. Garcia authored and Jay S. Bryant committed Nov 18, 2013
1 parent dbc119b commit 7f4f083
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 0 deletions.
74 changes: 74 additions & 0 deletions cinder/api/openstack/wsgi.py
Expand Up @@ -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:
Expand Down
34 changes: 34 additions & 0 deletions cinder/tests/api/openstack/test_wsgi.py
Expand Up @@ -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):
Expand Down

0 comments on commit 7f4f083

Please sign in to comment.