placement/placement/exception.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

204 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.
"""Exceptions for use in the Placement API."""
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
class _BaseException(Exception):
"""Base Exception
To correctly use this class, inherit from it and define
a 'msg_fmt' property. That msg_fmt will get printf'd
with the keyword arguments provided to the constructor.
"""
msg_fmt = "An unknown exception occurred."
def __init__(self, message=None, **kwargs):
self.kwargs = kwargs
if not message:
try:
message = self.msg_fmt % kwargs
except Exception:
# NOTE(melwitt): This is done in a separate method so it can be
# monkey-patched during testing to make it a hard failure.
self._log_exception()
message = self.msg_fmt
self.message = message
super(_BaseException, self).__init__(message)
def _log_exception(self):
# kwargs doesn't match a variable in the message
# log the issue and the kwargs
LOG.exception('Exception in string format operation')
for name, value in self.kwargs.items():
LOG.error("%s: %s" % (name, value)) # noqa
def format_message(self):
# Use the first argument to the python Exception object which
# should be our full exception message, (see __init__).
return self.args[0]
class NotFound(_BaseException):
msg_fmt = "Resource could not be found."
class Exists(_BaseException):
msg_fmt = "Resource already exists."
class InvalidInventory(_BaseException):
msg_fmt = ("Inventory for '%(resource_class)s' on "
"resource provider '%(resource_provider)s' invalid.")
class CannotDeleteParentResourceProvider(_BaseException):
msg_fmt = ("Cannot delete resource provider that is a parent of "
"another. Delete child providers first.")
class ConcurrentUpdateDetected(_BaseException):
msg_fmt = ("Another thread concurrently updated the data. "
"Please retry your update")
class ResourceProviderConcurrentUpdateDetected(ConcurrentUpdateDetected):
msg_fmt = ("Another thread concurrently updated the resource provider "
"data. Please retry your update")
class ResourceProviderNotFound(NotFound):
# Marker exception indicating that we've filtered down to zero possible
# allocation candidates. Does not represent an API error; should only be
# used internally: no results is a 200 with empty allocation_requests.
msg_fmt = "No results are possible."
class InvalidAllocationCapacityExceeded(InvalidInventory):
msg_fmt = ("Unable to create allocation for '%(resource_class)s' on "
"resource provider '%(resource_provider)s'. The requested "
"amount would exceed the capacity.")
class InvalidAllocationConstraintsViolated(InvalidInventory):
msg_fmt = ("Unable to create allocation for '%(resource_class)s' on "
"resource provider '%(resource_provider)s'. The requested "
"amount would violate inventory constraints.")
class InvalidInventoryCapacity(InvalidInventory):
msg_fmt = ("Invalid inventory for '%(resource_class)s' on "
"resource provider '%(resource_provider)s'. "
"The reserved value is greater than or equal to total.")
class InvalidInventoryCapacityReservedCanBeTotal(InvalidInventoryCapacity):
msg_fmt = ("Invalid inventory for '%(resource_class)s' on "
"resource provider '%(resource_provider)s'. "
"The reserved value is greater than total.")
# An exception with this name is used on both sides of the placement/
# nova interaction.
class InventoryInUse(InvalidInventory):
msg_fmt = ("Inventory for '%(resource_classes)s' on "
"resource provider '%(resource_provider)s' in use.")
class InventoryWithResourceClassNotFound(NotFound):
msg_fmt = "No inventory of class %(resource_class)s found."
class MaxDBRetriesExceeded(_BaseException):
msg_fmt = ("Max retries of DB transaction exceeded attempting to "
"perform %(action)s.")
class ObjectActionError(_BaseException):
msg_fmt = 'Object action %(action)s failed because: %(reason)s'
class PolicyNotAuthorized(_BaseException):
msg_fmt = "Policy does not allow %(action)s to be performed."
class ResourceClassCannotDeleteStandard(_BaseException):
msg_fmt = "Cannot delete standard resource class %(resource_class)s."
class ResourceClassCannotUpdateStandard(_BaseException):
msg_fmt = "Cannot update standard resource class %(resource_class)s."
class ResourceClassExists(_BaseException):
msg_fmt = "Resource class %(resource_class)s already exists."
class ResourceClassInUse(_BaseException):
msg_fmt = ("Cannot delete resource class %(resource_class)s. "
"Class is in use in inventory.")
class ResourceClassNotFound(NotFound):
msg_fmt = "No such resource class %(name)s."
class ResourceProviderInUse(_BaseException):
msg_fmt = "Resource provider has allocations."
class TraitCannotDeleteStandard(_BaseException):
msg_fmt = "Cannot delete standard trait %(name)s."
class TraitExists(_BaseException):
msg_fmt = "The Trait %(name)s already exists"
class TraitInUse(_BaseException):
msg_fmt = "The trait %(name)s is in use by a resource provider."
class TraitNotFound(NotFound):
msg_fmt = "No such trait(s): %(name)s."
class ProjectNotFound(NotFound):
msg_fmt = "No such project(s): %(external_id)s."
class ProjectExists(Exists):
msg_fmt = "The project %(external_id)s already exists."
class UserNotFound(NotFound):
msg_fmt = "No such user(s): %(external_id)s."
class UserExists(Exists):
msg_fmt = "The user %(external_id)s already exists."
class ConsumerNotFound(NotFound):
msg_fmt = "No such consumer(s): %(uuid)s."
class ConsumerExists(Exists):
msg_fmt = "The consumer %(uuid)s already exists."