Files
neutron/neutron/common/cache_utils.py
Ihar Hrachyshka d034532d37 cache_utils: fixed cache misses for the new (oslo.cache) configuration
When the new (oslo.cache) way of configuring the cache is used, cache is
never hit, because self._cache.get() consistently raises exceptions:

TypeError: 'sha1() argument 1 must be string or buffer, not tuple'

It occurs because the key passed into the oslo.cache region does not
conform to oslo.cache requirements. The library enforces the key to be
compatible with sha1_mangle_key() function:

http://git.openstack.org/cgit/openstack/oslo.cache/tree/oslo_cache/core.py?id=8b8a718507b30a4a2fd36e6c14d1071bd6cca878#n140

With this patch, we transform the key to a string, to conform to the
requirements.

The bug sneaked into the tree unnoticed because of two reasons:

- there were no unit tests to validate the new way of cache
  configuration.
- the 'legacy' code path was configuring the cache in a slightly
  different way, omitting some oslo.cache code.

For the former, new unit tests were introduced that cover the cache on
par with the legacy mode.

For the latter, the legacy code path was modified to rely on the same
configuration path as for the new way.

Closes-Bug: #1593342
Change-Id: I2724aa21f66f0fb69147407bfcf3184585d7d5cd
2016-06-16 23:07:40 +02:00

158 lines
5.8 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
from oslo_cache import core as cache
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import reflection
from six.moves.urllib import parse
from neutron._i18n import _
from neutron.common import utils
cache_opts = [
cfg.StrOpt('cache_url', default='',
deprecated_for_removal=True,
help=_('URL to connect to the cache back end. '
'This option is deprecated in the Newton release and '
'will be removed. Please add a [cache] group for '
'oslo.cache in your neutron.conf and add "enable" and '
'"backend" options in this section.')),
]
LOG = logging.getLogger(__name__)
def register_oslo_configs(conf):
conf.register_opts(cache_opts)
cache.configure(conf)
def get_cache(conf):
"""Used to get cache client"""
# cache_url is still used, we just respect it. Memory backend is the only
# backend supported before and default_ttl is the only options of Memory
# backend. We use dict backend of oslo.cache for this.
if conf.cache_url:
return _get_cache_region_for_legacy(conf.cache_url)
elif conf.cache.enabled:
return _get_cache_region(conf)
else:
return False
def _get_cache_region(conf):
region = cache.create_region()
cache.configure_cache_region(conf, region)
return region
def _get_cache_region_for_legacy(url):
parsed = parse.urlparse(url)
backend = parsed.scheme
if backend == 'memory':
query = parsed.query
# NOTE(fangzhen): The following NOTE and code is from legacy
# oslo-incubator cache module. Previously reside in neutron at
# neutron/openstack/common/cache/cache.py:78
# NOTE(flaper87): We need the following hack
# for python versions < 2.7.5. Previous versions
# of python parsed query params just for 'known'
# schemes. This was changed in this patch:
# http://hg.python.org/cpython/rev/79e6ff3d9afd
if not query and '?' in parsed.path:
query = parsed.path.split('?', 1)[-1]
parameters = parse.parse_qs(query)
conf = cfg.ConfigOpts()
register_oslo_configs(conf)
cache_conf_dict = {
'enabled': True,
'backend': 'oslo_cache.dict',
'expiration_time': int(parameters.get('default_ttl', [0])[0]),
}
for k, v in cache_conf_dict.items():
conf.set_override(k, v, group='cache')
return _get_cache_region(conf)
else:
raise RuntimeError(_('Old style configuration can use only memory '
'(dict) backend'))
class cache_method_results(object):
"""This decorator is intended for object methods only."""
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
self._first_call = True
self._not_cached = cache.NO_VALUE
def _get_from_cache(self, target_self, *args, **kwargs):
target_self_cls_name = reflection.get_class_name(target_self,
fully_qualified=False)
func_name = "%(module)s.%(class)s.%(func_name)s" % {
'module': target_self.__module__,
'class': target_self_cls_name,
'func_name': self.func.__name__,
}
key = (func_name,) + args
if kwargs:
key += utils.dict2tuple(kwargs)
# oslo.cache expects a string or a buffer
key = str(key)
try:
item = target_self._cache.get(key)
except TypeError:
LOG.debug("Method %(func_name)s cannot be cached due to "
"unhashable parameters: args: %(args)s, kwargs: "
"%(kwargs)s",
{'func_name': func_name,
'args': args,
'kwargs': kwargs})
return self.func(target_self, *args, **kwargs)
if item is self._not_cached:
item = self.func(target_self, *args, **kwargs)
target_self._cache.set(key, item)
return item
def __call__(self, target_self, *args, **kwargs):
target_self_cls_name = reflection.get_class_name(target_self,
fully_qualified=False)
if not hasattr(target_self, '_cache'):
raise NotImplementedError(
_("Instance of class %(module)s.%(class)s must contain _cache "
"attribute") % {
'module': target_self.__module__,
'class': target_self_cls_name})
if not target_self._cache:
if self._first_call:
LOG.debug("Instance of class %(module)s.%(class)s doesn't "
"contain attribute _cache therefore results "
"cannot be cached for %(func_name)s.",
{'module': target_self.__module__,
'class': target_self_cls_name,
'func_name': self.func.__name__})
self._first_call = False
return self.func(target_self, *args, **kwargs)
return self._get_from_cache(target_self, *args, **kwargs)
def __get__(self, obj, objtype):
return functools.partial(self.__call__, obj)