placement/placement/attribute_cache.py
Chris Dent b09f2f917f Make a TraitCache similar to ResourceClassCache
The Trait and ResourceClass db objects have the same essential
structure and are used throughout the code in similar ways:

* turn a name into an id
* turn an id into a name
* get an unfiltered list of names
* make a mapping of ids to names

In I409a5e819a72d64e66ee390e4528da0c503d8d05 we made the resource
class cache request specific. Doing that work made it pretty clear
we could have a similar cache for traits and as a result visit the
traits db fewer times per request.

The implementation is straightforward: make an _AttributeCache super
class that is a parent to ResourceClassCache. Create TraitCache as
a sibling. The sole difference is the table used for the data authority
and the exception raised when an attribute is not found.

The new super class has been refactored to use private attributes and
methods.

A 'get_all' method is added to the cache to list the full collection
of dicts it contains. That can be be directly transformed into Trait
and ResourceClass objects. The order of the results of this method
are not predictable, and sorting them would add cost for no benefit,
so gabbi tests which had previously relied on the ordered of returned
resource classes have been removed.

From the API, listing traits and resource classes (without filters) now
uses the cache instead of going to the db. Where filters (in traits) are
required, the db is accessed.

The research_context turns lists of trait names into id, name maps for
required and forbidden traits.

Further, anywhere the traits table was joined to create a name of an id,
the cache is used instead. This allows to drop some joins and operate
fully in-process and in-RAM. No additional loops are added to make this
happen: the translation is done in existing loops.

The upshot of these changes is that unless there is a write operation on
a trait or resource class, both tables are scanned at most once in any
request. And when they are scanned it is to list their entire contents.

As noted in the _AttributeCache docstring there are restrictions
on what kinds of entities can use the cache and some necessary
precautions.

Change-Id: Ia19ea2b4ecdde25323579edf60ad6269d05e75a2
2019-07-24 11:23:50 +01:00

164 lines
6.4 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 sqlalchemy as sa
from placement.db.sqlalchemy import models
from placement import db_api
from placement import exception
_RC_TBL = models.ResourceClass.__table__
_TRAIT_TBL = models.Trait.__table__
class _AttributeCache(object):
"""A cache of integer and string lookup values for string-based attributes.
Subclasses must define `_table` and `_not_found` members describing the
database table which is the authoritative source of data and the exception
raised if data for an attribute is not found, respectively.
The cache is required to be correct for the extent of any individual API
request and be used only for those entities where any change to the
underlying data is only making that change and will have no subsequent
queries into the cache. For example, when we add a new resource class we
do not then list all the resource classes from within the same session.
Despite that requirement, any time an entity associated with a cache is
created, updated, or deleted `clear()` should be called on the cache.
"""
_table = None
_not_found = None
def __init__(self, ctx):
"""Initialize the cache of resource class identifiers.
:param ctx: `placement.context.RequestContext` from which we can grab a
`SQLAlchemy.Connection` object to use for any DB lookups.
"""
# Prevent this class being created directly, relevent during
# development.
assert self._table is not None, "_table must be defined"
assert self._not_found is not None, "_not_found must be defined"
self._ctx = ctx
self.clear()
def clear(self):
self._id_cache = {}
self._str_cache = {}
self._all_cache = {}
def id_from_string(self, attr_str):
"""Given a string representation of an attribute -- e.g. "DISK_GB"
or "CUSTOM_IRON_SILVER" -- return the integer code for the attribute
by doing a DB lookup into the appropriate table; however, the results
of these DB lookups are cached since the lookups are so frequent.
:param attr_str: The string representation of the attribute to look up
a numeric identifier for.
:returns Integer identifier for the attribute.
:raises An instance of the subclass' _not_found exception if attribute
cannot be found in the DB.
"""
attr_id = self._id_cache.get(attr_str)
if attr_id is not None:
return attr_id
# Otherwise, check the database table
self._refresh_from_db(self._ctx)
if attr_str in self._id_cache:
return self._id_cache[attr_str]
raise self._not_found(name=attr_str)
def all_from_string(self, attr_str):
"""Given a string representation of an attribute -- e.g. "DISK_GB"
or "CUSTOM_IRON_SILVER" -- return all the attribute info.
:param attr_str: The string representation of the attribute for which
to look up the object.
:returns: dict representing the attribute fields, if the attribute was
found in the appropriate database table.
:raises An instance of the subclass' _not_found exception if attr_str
cannot be found in the DB.
"""
attr_id_str = self._all_cache.get(attr_str)
if attr_id_str is not None:
return attr_id_str
# Otherwise, check the database table
self._refresh_from_db(self._ctx)
if attr_str in self._all_cache:
return self._all_cache[attr_str]
raise self._not_found(name=attr_str)
def string_from_id(self, attr_id):
"""The reverse of the id_from_string() method. Given a supplied numeric
identifier for an attribute, we look up the corresponding string
representation, via a DB lookup. The results of these DB lookups are
cached since the lookups are so frequent.
:param attr_id: The numeric representation of the attribute to look
up a string identifier for.
:returns: String identifier for the attribute.
:raises An instances of the subclass' _not_found exception if attr_id
cannot be found in the DB.
"""
attr_str = self._str_cache.get(attr_id)
if attr_str is not None:
return attr_str
# Otherwise, check the database table
self._refresh_from_db(self._ctx)
if attr_id in self._str_cache:
return self._str_cache[attr_id]
raise self._not_found(name=attr_id)
def get_all(self):
"""Return an iterator of all the resources in the cache with all their
attributes.
In Python3 the return value is a generator.
"""
if not self._all_cache:
self._refresh_from_db(self._ctx)
return self._all_cache.values()
@db_api.placement_context_manager.reader
def _refresh_from_db(self, ctx):
"""Grabs all resource classes or traits from the respective DB table
and populates the supplied cache object's internal integer and string
identifier dicts.
:param ctx: RequestContext with the the database session.
"""
table = self._table
sel = sa.select([table.c.id, table.c.name, table.c.updated_at,
table.c.created_at])
res = ctx.session.execute(sel).fetchall()
self._id_cache = {r[1]: r[0] for r in res}
self._str_cache = {r[0]: r[1] for r in res}
self._all_cache = {r[1]: r for r in res}
class ResourceClassCache(_AttributeCache):
"""An _AttributeCache for resource classes."""
_table = _RC_TBL
_not_found = exception.ResourceClassNotFound
class TraitCache(_AttributeCache):
"""An _AttributeCache for traits."""
_table = _TRAIT_TBL
_not_found = exception.TraitNotFound