Merge "placement: add cache for resource classes"
This commit is contained in:
97
nova/db/sqlalchemy/resource_class_cache.py
Normal file
97
nova/db/sqlalchemy/resource_class_cache.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# 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 sqlalchemy as sa
|
||||
|
||||
from nova.db.sqlalchemy import api as db_api
|
||||
from nova.db.sqlalchemy import api_models as models
|
||||
from nova.objects import fields
|
||||
|
||||
_RC_TBL = models.ResourceClass.__table__
|
||||
|
||||
|
||||
@db_api.api_context_manager.reader
|
||||
def _refresh_from_db(ctx, cache):
|
||||
"""Grabs all custom resource classes from the DB table and populates the
|
||||
supplied cache object's internal integer and string identifier dicts.
|
||||
|
||||
:param cache: ResourceClassCache object to refresh.
|
||||
"""
|
||||
with ctx.session.connection() as conn:
|
||||
sel = sa.select([_RC_TBL.c.id, _RC_TBL.c.name])
|
||||
res = conn.execute(sel).fetchall()
|
||||
cache.id_cache = {r[1]: r[0] for r in res}
|
||||
cache.str_cache = {r[0]: r[1] for r in res}
|
||||
|
||||
|
||||
class ResourceClassCache(object):
|
||||
"""A cache of integer and string lookup values for resource classes."""
|
||||
|
||||
def __init__(self, ctx):
|
||||
"""Initialize the cache of resource class identifiers.
|
||||
|
||||
:param ctx: `nova.context.RequestContext` from which we can grab a
|
||||
`SQLAlchemy.Connection` object to use for any DB lookups.
|
||||
"""
|
||||
self.ctx = ctx
|
||||
self.id_cache = {}
|
||||
self.str_cache = {}
|
||||
|
||||
def id_from_string(self, rc_str):
|
||||
"""Given a string representation of a resource class -- e.g. "DISK_GB"
|
||||
or "IRON_SILVER" -- return the integer code for the resource class. For
|
||||
standard resource classes, this integer code will match the list of
|
||||
resource classes on the fields.ResourceClass field type. Other custom
|
||||
resource classes will cause a DB lookup into the resource_classes
|
||||
table, however the results of these DB lookups are cached since the
|
||||
lookups are so frequent.
|
||||
|
||||
:param rc_str: The string representation of the resource class to look
|
||||
up a numeric identifier for.
|
||||
:returns integer identifier for the resource class, or None, if no such
|
||||
resource class was found in the list of standard resource
|
||||
classes or the resource_classes database table.
|
||||
"""
|
||||
if rc_str in self.id_cache:
|
||||
return self.id_cache[rc_str]
|
||||
|
||||
# First check the standard resource classes
|
||||
if rc_str in fields.ResourceClass.ALL:
|
||||
return fields.ResourceClass.ALL.index(rc_str)
|
||||
else:
|
||||
# Otherwise, check the database table
|
||||
_refresh_from_db(self.ctx, self)
|
||||
return self.id_cache.get(rc_str)
|
||||
|
||||
def string_from_id(self, rc_id):
|
||||
"""The reverse of the id_from_string() method. Given a supplied numeric
|
||||
identifier for a resource class, we look up the corresponding string
|
||||
representation, either in the list of standard resource classes or via
|
||||
a DB lookup. The results of these DB lookups are cached since the
|
||||
lookups are so frequent.
|
||||
|
||||
:param rc_id: The numeric representation of the resource class to look
|
||||
up a string identifier for.
|
||||
:returns string identifier for the resource class, or None, if no such
|
||||
resource class was found in the list of standard resource
|
||||
classes or the resource_classes database table.
|
||||
"""
|
||||
if rc_id in self.str_cache:
|
||||
return self.str_cache[rc_id]
|
||||
|
||||
# First check the fields.ResourceClass.ALL enum
|
||||
try:
|
||||
return fields.ResourceClass.ALL[rc_id]
|
||||
except IndexError:
|
||||
# Otherwise, check the database table
|
||||
_refresh_from_db(self.ctx, self)
|
||||
return self.str_cache.get(rc_id)
|
71
nova/tests/functional/db/test_resource_class_cache.py
Normal file
71
nova/tests/functional/db/test_resource_class_cache.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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 mock
|
||||
|
||||
from nova.db.sqlalchemy import resource_class_cache as rc_cache
|
||||
from nova import test
|
||||
from nova.tests import fixtures
|
||||
|
||||
|
||||
class TestResourceClassCache(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceClassCache, self).setUp()
|
||||
self.db = self.useFixture(fixtures.Database(database='api'))
|
||||
self.context = mock.Mock()
|
||||
sess_mock = mock.Mock()
|
||||
sess_mock.connection.side_effect = self.db.get_engine().connect
|
||||
self.context.session = sess_mock
|
||||
|
||||
@mock.patch('sqlalchemy.select')
|
||||
def test_rc_cache_std_no_db(self, sel_mock):
|
||||
"""Test that looking up either an ID or a string in the resource class
|
||||
cache for a standardized resource class does not result in a DB
|
||||
call.
|
||||
"""
|
||||
cache = rc_cache.ResourceClassCache(self.context)
|
||||
|
||||
self.assertEqual('VCPU', cache.string_from_id(0))
|
||||
self.assertEqual('MEMORY_MB', cache.string_from_id(1))
|
||||
self.assertEqual(0, cache.id_from_string('VCPU'))
|
||||
self.assertEqual(1, cache.id_from_string('MEMORY_MB'))
|
||||
|
||||
self.assertFalse(sel_mock.called)
|
||||
|
||||
def test_rc_cache_custom(self):
|
||||
"""Test that non-standard, custom resource classes hit the database and
|
||||
return appropriate results, caching the results after a single
|
||||
query.
|
||||
"""
|
||||
cache = rc_cache.ResourceClassCache(self.context)
|
||||
|
||||
# Haven't added anything to the DB yet, so should return None
|
||||
self.assertIsNone(cache.string_from_id(1001))
|
||||
self.assertIsNone(cache.id_from_string("IRON_NFV"))
|
||||
|
||||
# Now add to the database and verify appropriate results...
|
||||
with self.context.session.connection() as conn:
|
||||
ins_stmt = rc_cache._RC_TBL.insert().values(
|
||||
id=1001,
|
||||
name='IRON_NFV'
|
||||
)
|
||||
conn.execute(ins_stmt)
|
||||
|
||||
self.assertEqual('IRON_NFV', cache.string_from_id(1001))
|
||||
self.assertEqual(1001, cache.id_from_string('IRON_NFV'))
|
||||
|
||||
# Try same again and verify we don't hit the DB.
|
||||
with mock.patch('sqlalchemy.select') as sel_mock:
|
||||
self.assertEqual('IRON_NFV', cache.string_from_id(1001))
|
||||
self.assertEqual(1001, cache.id_from_string('IRON_NFV'))
|
||||
self.assertFalse(sel_mock.called)
|
Reference in New Issue
Block a user