Switch ConsumerType to use an AttributeCache

ConsumerType has the same structure and behavior as a Trait
or ResourceClass and, like them, we really only ever care about
the name and/or the id. This makes it a good candidate to use
the AttributeCache, simplifying the code a fair amount.

Co-Authored-By: melanie witt <melwittt@gmail.com>

Change-Id: I32499b2616e253a06f8a003fd3eef91006b58f42
This commit is contained in:
Chris Dent 2019-08-30 18:10:26 +01:00 committed by melanie witt
parent 3772132579
commit edb09c5f91
7 changed files with 39 additions and 81 deletions

View File

@ -15,7 +15,9 @@ import sqlalchemy as sa
from placement.db.sqlalchemy import models
from placement import db_api
from placement import exception
from placement.objects import consumer_type as ct_obj
_CONSUMER_TYPE_TBL = models.ConsumerType.__table__
_RC_TBL = models.ResourceClass.__table__
_TRAIT_TBL = models.Trait.__table__
@ -149,6 +151,20 @@ class _AttributeCache(object):
self._all_cache = {r[1]: r for r in res}
class ConsumerTypeCache(_AttributeCache):
"""An _AttributeCache for consumer types."""
_table = _CONSUMER_TYPE_TBL
_not_found = exception.ConsumerTypeNotFound
@db_api.placement_context_manager.reader
def _refresh_from_db(self, ctx):
super(ConsumerTypeCache, self)._refresh_from_db(ctx)
# The consumer_type_id is nullable and records with a NULL (None)
# consumer_type_id are considered as 'unknown'.
self._id_cache[None] = ct_obj.NULL_CONSUMER_TYPE_ALIAS
class ResourceClassCache(_AttributeCache):
"""An _AttributeCache for resource classes."""

View File

@ -23,6 +23,7 @@ class RequestContext(context.RequestContext):
def __init__(self, *args, **kwargs):
self.config = kwargs.pop('config', None)
self.ct_cache = attribute_cache.ConsumerTypeCache(self)
self.rc_cache = attribute_cache.ResourceClassCache(self)
self.trait_cache = attribute_cache.TraitCache(self)
super(RequestContext, self).__init__(*args, **kwargs)

View File

@ -28,7 +28,6 @@ from placement import exception
from placement.handlers import util as data_util
from placement import microversion
from placement.objects import allocation as alloc_obj
from placement.objects import consumer_type
from placement.objects import resource_provider as rp_obj
from placement.policies import allocation as policies
from placement.schemas import allocation as schema
@ -111,16 +110,9 @@ def _serialize_allocations_for_consumer(context, allocations, want_version):
result['consumer_generation'] = consumer.generation
show_consumer_type = want_version.matches((1, 38))
if show_consumer_type:
# TODO(cdent): This should either access a subclass of
# AttributeCache or the data returned from the persistence layer
# should already have a name. We want to avoid accessing the
# database from the handler layer repeated times.
if consumer.consumer_type_id:
con_type = consumer_type.ConsumerType.get_by_id(
context, consumer.consumer_type_id).name
else:
con_type = consumer_type.DEFAULT_CONSUMER_TYPE
result['consumer_type'] = con_type
con_name = context.ct_cache.string_from_id(
consumer.consumer_type_id)
result['consumer_type'] = con_name
return result

View File

@ -34,15 +34,15 @@ def fetch_consumer_type_id(ctx, name):
:returns: The id of the ConsumerType object.
"""
try:
cons_type = consumer_type_obj.ConsumerType.get_by_name(ctx, name)
return ctx.ct_cache.id_from_string(name)
except exception.ConsumerTypeNotFound:
cons_type = consumer_type_obj.ConsumerType(ctx, name=name)
try:
cons_type.create()
return cons_type.id
except exception.ConsumerTypeExists:
# another thread created concurrently, so try again
return fetch_consumer_type_id(ctx, name)
return cons_type.id
def _get_or_create_project(ctx, project_id):

View File

@ -11,7 +11,6 @@
# under the License.
from oslo_db import exception as db_exc
import sqlalchemy as sa
from placement.db.sqlalchemy import models
from placement import db_api
@ -23,52 +22,6 @@ _CONSUMER_TYPES_SYNCED = False
NULL_CONSUMER_TYPE_ALIAS = 'unknown'
@db_api.placement_context_manager.reader
def _get_consumer_type_by_id(ctx, id):
# The SQL for this looks like the following:
# SELECT
# c.id, c.name,
# c.updated_at, c.created_at
# FROM consumer_types c
# WHERE c.id = $id
consumer_types = sa.alias(CONSUMER_TYPE_TBL, name="c")
cols = [
consumer_types.c.id,
consumer_types.c.name,
consumer_types.c.updated_at,
consumer_types.c.created_at
]
sel = sa.select(cols).where(consumer_types.c.id == id)
res = ctx.session.execute(sel).fetchone()
if not res:
raise exception.ConsumerTypeNotFound(name=id)
return dict(res)
@db_api.placement_context_manager.reader
def _get_consumer_type_by_name(ctx, name):
# The SQL for this looks like the following:
# SELECT
# c.id, c.name,
# c.updated_at, c.created_at
# FROM consumer_types c
# WHERE c.name = $name
consumer_types = sa.alias(CONSUMER_TYPE_TBL, name="c")
cols = [
consumer_types.c.id,
consumer_types.c.name,
consumer_types.c.updated_at,
consumer_types.c.created_at
]
sel = sa.select(cols).where(consumer_types.c.name == name)
res = ctx.session.execute(sel).fetchone()
if not res:
raise exception.ConsumerTypeNotFound(name=name)
return dict(res)
@db_api.placement_context_manager.writer
def _create_in_db(ctx, name):
db_obj = models.ConsumerType(name=name)
@ -99,16 +52,18 @@ class ConsumerType(object):
target._context = ctx
return target
# NOTE(cdent): get_by_id and get_by_name are not currently used
# but are left in place to indicate the smooth migration from
# direct db access to using the AttributeCache.
@classmethod
def get_by_id(cls, ctx, id):
res = _get_consumer_type_by_id(ctx, id)
return cls._from_db_object(ctx, cls(ctx), res)
return ctx.ct_cache.all_from_string(ctx.ct_cache.string_from_id(id))
@classmethod
def get_by_name(cls, ctx, name):
res = _get_consumer_type_by_name(ctx, name)
return cls._from_db_object(ctx, cls(ctx), res)
return ctx.ct_cache.all_from_string(name)
def create(self):
ct = _create_in_db(self._context, self.name)
return self._from_db_object(self._context, self, ct)
self._from_db_object(self._context, self, ct)
self._context.ct_cache.clear()

View File

@ -20,6 +20,8 @@ class ContextTestCase(testtools.TestCase):
def setUp(self):
super(ContextTestCase, self).setUp()
self.useFixture(
fixtures.MockPatch('placement.attribute_cache.ConsumerTypeCache'))
self.useFixture(
fixtures.MockPatch('placement.attribute_cache.ResourceClassCache'))
self.useFixture(

View File

@ -25,7 +25,6 @@ from placement import exception
from placement.handlers import util
from placement import microversion
from placement.objects import consumer as consumer_obj
from placement.objects import consumer_type as consumer_type_obj
from placement.objects import project as project_obj
from placement.objects import user as user_obj
from placement.tests.unit import base
@ -58,9 +57,6 @@ class TestEnsureConsumer(base.ContextTestCase):
self.mock_consumer_update = self.useFixture(fixtures.MockPatch(
'placement.objects.consumer.'
'Consumer.update')).mock
self.mock_consumer_type_get = self.useFixture(fixtures.MockPatch(
'placement.objects.consumer_type.'
'ConsumerType.get_by_name')).mock
self.ctx = context.RequestContext(user_id='fake', project_id='fake')
self.ctx.config = self.conf
self.consumer_id = uuidsentinel.consumer
@ -245,10 +241,6 @@ class TestEnsureConsumer(base.ContextTestCase):
self.ctx, id=1, project=proj, user=user, generation=1,
consumer_type_id=1)
self.mock_consumer_get.return_value = consumer
# Supplied consumer type ID = 2
consumer_type = consumer_type_obj.ConsumerType(
self.ctx, id=2, name='TYPE')
self.mock_consumer_type_get.return_value = consumer_type
consumer_gen = 1
util.ensure_consumer(
@ -256,8 +248,10 @@ class TestEnsureConsumer(base.ContextTestCase):
consumer_gen, 'TYPE', self.cons_type_req_version)
# Expect 1 call to update() to update to the supplied consumer type ID
self.mock_consumer_update.assert_called_once_with()
# Consumer should have the new consumer type ID = 2
self.assertEqual(2, consumer.consumer_type_id)
# Consumer should have the new consumer type from the cache
self.assertEqual(
self.ctx.ct_cache.id_from_string.return_value,
consumer.consumer_type_id)
def test_consumer_create_exists_different_consumer_type_supplied(self):
"""Tests that we update a consumer's type ID if the one supplied by a
@ -273,10 +267,6 @@ class TestEnsureConsumer(base.ContextTestCase):
self.ctx, id=1, project=proj, user=user, generation=1,
consumer_type_id=1, uuid=uuidsentinel.consumer)
self.mock_consumer_get.return_value = consumer
# Request B supplied consumer type ID = 2
consumer_type = consumer_type_obj.ConsumerType(
self.ctx, id=2, name='TYPE')
self.mock_consumer_type_get.return_value = consumer_type
# Request B will encounter ConsumerExists as Request A just created it
self.mock_consumer_create.side_effect = (
exception.ConsumerExists(uuid=uuidsentinel.consumer))
@ -287,5 +277,7 @@ class TestEnsureConsumer(base.ContextTestCase):
consumer_gen, 'TYPE', self.cons_type_req_version)
# Expect 1 call to update() to update to the supplied consumer type ID
self.mock_consumer_update.assert_called_once_with()
# Consumer should have the new consumer type ID = 2
self.assertEqual(2, consumer.consumer_type_id)
# Consumer should have the new consumer type from the cache
self.assertEqual(
self.ctx.ct_cache.id_from_string.return_value,
consumer.consumer_type_id)