Avoid UnhashableKeyWarning in api.nova.novaclient

In python3, novaclient APIVersion instance is not hashable.
If APIVersion insstance is passed to novaclient(),
UnhashableKeyWarning will be emitted.
An error message on UnhashableKeyWarning is emitted repeatedly
and this can pollute error log with unuseful messages.

To convert all unhashable arguments into hashable variables,
a new decorator memoized_with_argconv is introduced.
This decorator takes a converter function.

Change-Id: I773355b9332b3b195576b51cc81eda80aa4402ed
Closes-Bug: #1790929
This commit is contained in:
Akihiro Motoki 2018-09-05 20:21:55 +00:00
parent 78090176ea
commit d68447452c
3 changed files with 76 additions and 9 deletions

View File

@ -81,3 +81,36 @@ class MemoizedTests(test.TestCase):
self.assertEqual(output2[position], leader)
# check that some_other_func returned a memoized list.
self.assertIs(output1, output2)
def test_memoized_with_argcnv(self):
value_list = []
def converter(*args, **kwargs):
new_args = tuple(reversed(args))
new_kwargs = dict((k, v + 1) for k, v in kwargs.items())
return new_args, new_kwargs
@memoized.memoized_with_argconv(converter)
def target_func(*args, **kwargs):
value_list.append(1)
return args, kwargs
for i in range(3):
ret_args, ret_kwargs = target_func(1, 2, 3)
self.assertEqual((3, 2, 1), ret_args)
self.assertEqual({}, ret_kwargs)
self.assertEqual(1, len(value_list))
value_list = []
for i in range(3):
ret_args, ret_kwargs = target_func(a=1, b=2, c=3)
self.assertEqual(tuple(), ret_args)
self.assertEqual({'a': 2, 'b': 3, 'c': 4}, ret_kwargs)
self.assertEqual(1, len(value_list))
value_list = []
for i in range(3):
ret_args, ret_kwargs = target_func(1, 2, a=3, b=4)
self.assertEqual((2, 1), ret_args)
self.assertEqual({'a': 4, 'b': 5}, ret_kwargs)
self.assertEqual(1, len(value_list))

View File

@ -178,3 +178,31 @@ def memoized_with_request(request_func, request_index=0):
return wrapped
return wrapper
def memoized_with_argconv(convert_func):
"""Decorator for caching functions which receive unhashable arguments
This decorator is a generalized version of memoized_with_request.
There are cases where argument(s) other than 'request' are also unhashable.
For such cases, such arguments also need to be converted into hashable
variables.
'convert_func' is responsible for replacing unhashable arguments
into corresponding hashable variables.
'convert_func' receives original arguments as its arguments and
it must return a full arguments including converted arguments.
See openstack_dashboard.api.nova as an example.
"""
def wrapper(func):
memoized_func = memoized(func)
@functools.wraps(func)
def wrapped(*args, **kwargs):
args, kwargs = convert_func(*args, **kwargs)
return memoized_func(*args, **kwargs)
return wrapped
return wrapper

View File

@ -36,8 +36,7 @@ from novaclient.v2 import servers as nova_servers
from horizon import exceptions as horizon_exceptions
from horizon.utils import functions as utils
from horizon.utils.memoized import memoized
from horizon.utils.memoized import memoized_with_request
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import microversions
@ -58,7 +57,7 @@ INSECURE = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
CACERT = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
@memoized
@memoized.memoized
def get_microversion(request, features):
client = novaclient(request)
min_ver, max_ver = api_versions._get_server_version_range(client)
@ -267,7 +266,14 @@ def get_auth_params_from_request(request):
)
@memoized_with_request(get_auth_params_from_request)
def _argconv_for_novaclient(request, version=None):
req_param = get_auth_params_from_request(request)
if isinstance(version, api_versions.APIVersion):
version = version.get_string()
return (req_param, version), {}
@memoized.memoized_with_argconv(_argconv_for_novaclient)
def novaclient(request_auth_params, version=None):
(
username,
@ -361,7 +367,7 @@ def flavor_get(request, flavor_id, get_extras=False):
@profiler.trace
@memoized
@memoized.memoized
def flavor_list(request, is_public=True, get_extras=False):
"""Get the list of available instance sizes (flavors)."""
flavors = novaclient(request).flavors.list(is_public=is_public)
@ -397,7 +403,7 @@ def update_pagination(entities, page_size, marker, sort_dir, sort_key,
@profiler.trace
@memoized
@memoized.memoized
def flavor_list_paged(request, is_public=True, get_extras=False, marker=None,
paginate=False, sort_key="name", sort_dir="desc",
reversed_order=False):
@ -427,7 +433,7 @@ def flavor_list_paged(request, is_public=True, get_extras=False, marker=None,
@profiler.trace
@memoized
@memoized.memoized
def flavor_access_list(request, flavor=None):
"""Get the list of access instance sizes (flavors)."""
return novaclient(request).flavor_access.list(flavor=flavor)
@ -1060,7 +1066,7 @@ def interface_detach(request, server, port_id):
@profiler.trace
@memoized_with_request(novaclient)
@memoized.memoized_with_request(novaclient)
def list_extensions(nova_api):
"""List all nova extensions, except the ones in the blacklist."""
blacklist = set(getattr(settings,
@ -1080,7 +1086,7 @@ def _list_extensions_wrap(request):
@profiler.trace
@memoized_with_request(_list_extensions_wrap, 1)
@memoized.memoized_with_request(_list_extensions_wrap, 1)
def extension_supported(extension_name, supported_ext_names):
"""Determine if nova supports a given extension name.