New Quota driver `DbQuotaNoLockDriver
`
This new quota driver, ``DbQuotaNoLockDriver``, does not create a lock per (resource, project_id) but retrieves the instant (resource, project_id) usage and the current (resource, project_id) reservations. If the requested number of resources fit the available quota, a new ``Reservation`` register is created with the amount of units requested. All those operations are done inside a DB transaction context. That means the amount of resources and reservations is guaranteed inside this transaction (depending on the DB backend isolation level defined) and the new reservation created will not clash with other DB transation. That will guarantee the number of resources and instant reservations never exceed the quota limits defined for this (resource, project_id). NOTES: - This change tries to be as unobtrusive as possible. The new driver uses the same ``DbQuotaDriver`` dabatase tables (except for ``QuotaUsage``) and the same Quota engine API, located in ``neutron.quota``. However, the Quota engine resources implements some particular API actions like "dirty", that are not used in the new driver. - The Pecan Quota enforcement hooks, ``neutron.pecan_wgsi.hooks.quota_enforcement``, execute actions like "resync", "mark_resources_dirty" or "set_resources_dirty", that has no meaning in the new driver. - The isolation between the Quota engine and the Pecan hook, and the driver itself is not clearly defined. A refactor of the Quota engine, Quota service, Quota drivers and a common API between the driver and the engine is needed. - If ``DbQuotaDriver`` is deprecated, ``CountableResource`` and ``TrackedResource`` will be joined in a single class. This resource class will have a count method (countable) or a hard dependency on a database table (tracked resource). The only difference will be the "count" method implementation. Closes-Bug: #1926787 Change-Id: I4f98c6fcd781459fd7150aff426d19c7fdfa98c1
This commit is contained in:
parent
9e6b7a2284
commit
e135a8221d
@ -43,7 +43,7 @@ limits are currently not enforced on RPC interfaces listening on the AMQP
|
|||||||
bus.
|
bus.
|
||||||
|
|
||||||
Plugin and ML2 drivers are not supposed to enforce quotas for resources they
|
Plugin and ML2 drivers are not supposed to enforce quotas for resources they
|
||||||
manage. However, the subnet_allocation [#]_ extension is an exception and will
|
manage. However, the subnet_allocation [1]_ extension is an exception and will
|
||||||
be discussed below.
|
be discussed below.
|
||||||
|
|
||||||
The quota management and enforcement mechanisms discussed here apply to every
|
The quota management and enforcement mechanisms discussed here apply to every
|
||||||
@ -59,12 +59,14 @@ There are two main components in the Neutron quota system:
|
|||||||
* The Quota Engine.
|
* The Quota Engine.
|
||||||
|
|
||||||
Both components rely on a quota driver. The neutron codebase currently defines
|
Both components rely on a quota driver. The neutron codebase currently defines
|
||||||
two quota drivers:
|
three quota drivers:
|
||||||
|
|
||||||
* neutron.db.quota.driver.DbQuotaDriver
|
* neutron.db.quota.driver.DbQuotaDriver
|
||||||
|
* neutron.db.quota.driver_nolock.DbQuotaNoLockDriver (default)
|
||||||
* neutron.quota.ConfDriver
|
* neutron.quota.ConfDriver
|
||||||
|
|
||||||
The latter driver is however deprecated.
|
The latter driver is however deprecated. The ``DbQuotaNoLockDriver`` is the
|
||||||
|
default quota driver, defined in the configuration option ``quota_driver``.
|
||||||
|
|
||||||
The Quota API extension handles quota management, whereas the Quota Engine
|
The Quota API extension handles quota management, whereas the Quota Engine
|
||||||
component handles quota enforcement. This API extension is loaded like any
|
component handles quota enforcement. This API extension is loaded like any
|
||||||
@ -91,7 +93,7 @@ For a reservation to be successful, the total amount of resources requested,
|
|||||||
plus the total amount of resources reserved, plus the total amount of resources
|
plus the total amount of resources reserved, plus the total amount of resources
|
||||||
already stored in the database should not exceed the project's quota limit.
|
already stored in the database should not exceed the project's quota limit.
|
||||||
|
|
||||||
Finally, both quota management and enforcement rely on a "quota driver" [#]_,
|
Finally, both quota management and enforcement rely on a "quota driver" [2]_,
|
||||||
whose task is basically to perform database operations.
|
whose task is basically to perform database operations.
|
||||||
|
|
||||||
Quota Management
|
Quota Management
|
||||||
@ -100,14 +102,14 @@ Quota Management
|
|||||||
The quota management component is fairly straightforward.
|
The quota management component is fairly straightforward.
|
||||||
|
|
||||||
However, unlike the vast majority of Neutron extensions, it uses it own
|
However, unlike the vast majority of Neutron extensions, it uses it own
|
||||||
controller class [#]_.
|
controller class [3]_.
|
||||||
This class does not implement the POST operation. List, get, update, and
|
This class does not implement the POST operation. List, get, update, and
|
||||||
delete operations are implemented by the usual index, show, update and
|
delete operations are implemented by the usual index, show, update and
|
||||||
delete methods. These method simply call into the quota driver for either
|
delete methods. These method simply call into the quota driver for either
|
||||||
fetching project quotas or updating them.
|
fetching project quotas or updating them.
|
||||||
|
|
||||||
The _update_attributes method is called only once in the controller lifetime.
|
The _update_attributes method is called only once in the controller lifetime.
|
||||||
This method dynamically updates Neutron's resource attribute map [#]_ so that
|
This method dynamically updates Neutron's resource attribute map [4]_ so that
|
||||||
an attribute is added for every resource managed by the quota engine.
|
an attribute is added for every resource managed by the quota engine.
|
||||||
Request authorisation is performed in this controller, and only 'admin' users
|
Request authorisation is performed in this controller, and only 'admin' users
|
||||||
are allowed to modify quotas for projects. As the neutron policy engine is not
|
are allowed to modify quotas for projects. As the neutron policy engine is not
|
||||||
@ -131,11 +133,18 @@ Resource Usage Info
|
|||||||
Neutron has two ways of tracking resource usage info:
|
Neutron has two ways of tracking resource usage info:
|
||||||
|
|
||||||
* CountableResource, where resource usage is calculated every time quotas
|
* CountableResource, where resource usage is calculated every time quotas
|
||||||
limits are enforced by counting rows in the resource table and reservations
|
limits are enforced by counting rows in the resource table or resources
|
||||||
for that resource.
|
tables and reservations for that resource.
|
||||||
* TrackedResource, which instead relies on a specific table tracking usage
|
* TrackedResource, depends on the selected driver:
|
||||||
data, and performs explicitly counting only when the data in this table are
|
|
||||||
not in sync with actual used and reserved resources.
|
* DbQuotaDriver: the resource usage relies on a specific table tracking
|
||||||
|
usage data, and performs explicitly counting only when the data in this
|
||||||
|
table are not in sync with actual used and reserved resources.
|
||||||
|
* DbQuotaNoLockDriver: the resource usage is counted directly from the
|
||||||
|
database table associated to the resource. In this new driver,
|
||||||
|
CountableResource and TrackedResource could look similar but
|
||||||
|
TrackedResource depends on one single database model (table) and the
|
||||||
|
resource count is done directly on this table only.
|
||||||
|
|
||||||
Another difference between CountableResource and TrackedResource is that the
|
Another difference between CountableResource and TrackedResource is that the
|
||||||
former invokes a plugin method to count resources. CountableResource should be
|
former invokes a plugin method to count resources. CountableResource should be
|
||||||
@ -147,6 +156,9 @@ use CountableResource instances.
|
|||||||
Resource creation is performed by the create_resource_instance factory method
|
Resource creation is performed by the create_resource_instance factory method
|
||||||
in the neutron.quota.resource module.
|
in the neutron.quota.resource module.
|
||||||
|
|
||||||
|
DbQuotaDriver description
|
||||||
|
-------------------------
|
||||||
|
|
||||||
From a performance perspective, having a table tracking resource usage
|
From a performance perspective, having a table tracking resource usage
|
||||||
has some advantages, albeit not fundamental. Indeed the time required for
|
has some advantages, albeit not fundamental. Indeed the time required for
|
||||||
executing queries to explicitly count objects will increase with the number of
|
executing queries to explicitly count objects will increase with the number of
|
||||||
@ -183,15 +195,48 @@ caveats' section, it is more reliable than solutions such as:
|
|||||||
* Having a periodic task synchronising quota usage data with actual data in
|
* Having a periodic task synchronising quota usage data with actual data in
|
||||||
the Neutron DB.
|
the Neutron DB.
|
||||||
|
|
||||||
Finally, regardless of whether CountableResource or TrackedResource is used,
|
|
||||||
the quota engine always invokes its count() method to retrieve resource usage.
|
DbQuotaNoLockDriver description
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
The strategy of this quota driver is the opposite to ``DbQuotaDriver``.
|
||||||
|
Instead of tracking the usage quota of each resource in a specific table,
|
||||||
|
this driver retrieves the used resources directly form the database.
|
||||||
|
Each TrackedResource is linked to a database table that stores the tracked
|
||||||
|
resources. This driver claims that a trivial query on the resource table,
|
||||||
|
filtering by project ID, is faster than attending to the DB events and tracking
|
||||||
|
the quota usage in an independent table.
|
||||||
|
|
||||||
|
This driver relays on the database engine transactionality isolation. Each
|
||||||
|
time a new resource is requested, the quota driver opens a database transaction
|
||||||
|
to:
|
||||||
|
|
||||||
|
* Clean up the expired reservations. The amount of expired reservations is
|
||||||
|
always limited because of the short timeout set (2 minutes).
|
||||||
|
* Retrieve the used resources for a specific project. This query retrieves
|
||||||
|
only the "project_id" column of the resource to avoid backref requests; that
|
||||||
|
limits the scope of the query and speeds up it.
|
||||||
|
* Retrieve the reserved resources, created by other concurrent operations.
|
||||||
|
* If there is enough quota, create a new reservation register.
|
||||||
|
|
||||||
|
Those operations, executed in the same transaction, are fast enough to avoid
|
||||||
|
another concurrent resource reservation, exceeding the available quota. At the
|
||||||
|
same time, this driver does not create a lock per resource and project ID,
|
||||||
|
allowing concurrent requests that won't be blocked by the resource lock.
|
||||||
|
Because the quota reservation process, described before, is a fast operation,
|
||||||
|
the chances of overcommiting resources over the quota limits are low. Neutron
|
||||||
|
does not enforce quota in such way that a quota limit violation could never
|
||||||
|
occur [5]_.
|
||||||
|
|
||||||
|
Regardless of whether CountableResource or TrackedResource is used, the quota
|
||||||
|
engine always invokes its count() method to retrieve resource usage.
|
||||||
Therefore, from the perspective of the Quota engine there is absolutely no
|
Therefore, from the perspective of the Quota engine there is absolutely no
|
||||||
difference between CountableResource and TrackedResource.
|
difference between CountableResource and TrackedResource.
|
||||||
|
|
||||||
Quota Enforcement
|
Quota Enforcement in DbQuotaDriver
|
||||||
-----------------
|
----------------------------------
|
||||||
|
|
||||||
Before dispatching a request to the plugin, the Neutron 'base' controller [#]_
|
Before dispatching a request to the plugin, the Neutron 'base' controller [6]_
|
||||||
attempts to make a reservation for requested resource(s).
|
attempts to make a reservation for requested resource(s).
|
||||||
Reservations are made by calling the make_reservation method in
|
Reservations are made by calling the make_reservation method in
|
||||||
neutron.quota.QuotaEngine.
|
neutron.quota.QuotaEngine.
|
||||||
@ -228,7 +273,7 @@ While non-locking approaches are possible, it has been found out that, since
|
|||||||
a non-locking algorithms increases the chances of collision, the cost of
|
a non-locking algorithms increases the chances of collision, the cost of
|
||||||
handling a DBDeadlock is still lower than the cost of retrying the operation
|
handling a DBDeadlock is still lower than the cost of retrying the operation
|
||||||
when a collision is detected. A study in this direction was conducted for
|
when a collision is detected. A study in this direction was conducted for
|
||||||
IP allocation operations, but the same principles apply here as well [#]_.
|
IP allocation operations, but the same principles apply here as well [7]_.
|
||||||
Nevertheless, moving away for DB-level locks is something that must happen
|
Nevertheless, moving away for DB-level locks is something that must happen
|
||||||
for quota enforcement in the future.
|
for quota enforcement in the future.
|
||||||
|
|
||||||
@ -345,9 +390,10 @@ Please be aware of the following limitations of the quota enforcement engine:
|
|||||||
References
|
References
|
||||||
----------
|
----------
|
||||||
|
|
||||||
.. [#] Subnet allocation extension: http://opendev.org/openstack/neutron/tree/neutron/extensions/subnetallocation.py
|
.. [1] Subnet allocation extension: http://opendev.org/openstack/neutron/tree/neutron/extensions/subnetallocation.py
|
||||||
.. [#] DB Quota driver class: http://opendev.org/openstack/neutron/tree/neutron/db/quota/driver.py#n30
|
.. [2] DB Quota driver class: http://opendev.org/openstack/neutron/tree/neutron/db/quota/driver.py#n30
|
||||||
.. [#] Quota API extension controller: http://opendev.org/openstack/neutron/tree/neutron/extensions/quotasv2.py#n40
|
.. [3] Quota API extension controller: http://opendev.org/openstack/neutron/tree/neutron/extensions/quotasv2.py#n40
|
||||||
.. [#] Neutron resource attribute map: http://opendev.org/openstack/neutron/tree/neutron/api/v2/attributes.py#n639
|
.. [4] Neutron resource attribute map: http://opendev.org/openstack/neutron/tree/neutron/api/v2/attributes.py#n639
|
||||||
.. [#] Base controller class: http://opendev.org/openstack/neutron/tree/neutron/api/v2/base.py#n50
|
.. [5] Quota limit exceeded: https://bugs.launchpad.net/neutron/+bug/1862050/
|
||||||
.. [#] http://lists.openstack.org/pipermail/openstack-dev/2015-February/057534.html
|
.. [6] Base controller class: http://opendev.org/openstack/neutron/tree/neutron/api/v2/base.py#n50
|
||||||
|
.. [7] http://lists.openstack.org/pipermail/openstack-dev/2015-February/057534.html
|
||||||
|
@ -18,8 +18,9 @@ from oslo_config import cfg
|
|||||||
|
|
||||||
from neutron._i18n import _
|
from neutron._i18n import _
|
||||||
|
|
||||||
QUOTA_DB_MODULE = 'neutron.db.quota.driver'
|
|
||||||
QUOTA_DB_DRIVER = '%s.DbQuotaDriver' % QUOTA_DB_MODULE
|
QUOTA_DB_MODULE = 'neutron.db.quota.driver_nolock'
|
||||||
|
QUOTA_DB_DRIVER = QUOTA_DB_MODULE + '.DbQuotaNoLockDriver'
|
||||||
QUOTA_CONF_DRIVER = 'neutron.quota.ConfDriver'
|
QUOTA_CONF_DRIVER = 'neutron.quota.ConfDriver'
|
||||||
QUOTAS_CFG_GROUP = 'QUOTAS'
|
QUOTAS_CFG_GROUP = 'QUOTAS'
|
||||||
|
|
||||||
|
78
neutron/db/quota/driver_nolock.py
Normal file
78
neutron/db/quota/driver_nolock.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Copyright (c) 2021 Red Hat Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from neutron_lib.db import api as db_api
|
||||||
|
from neutron_lib import exceptions
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from neutron.db.quota import api as quota_api
|
||||||
|
from neutron.db.quota import driver as quota_driver
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DbQuotaNoLockDriver(quota_driver.DbQuotaDriver):
|
||||||
|
"""Driver to enforce quotas and retrieve quota information
|
||||||
|
|
||||||
|
This driver does not use a (resource, project_id) lock but relays on the
|
||||||
|
simplicity of the calculation method, that is executed in a single database
|
||||||
|
transaction. The method (1) counts the number of created resources and (2)
|
||||||
|
the number of resource reservations. If the requested number of resources
|
||||||
|
do not exceeds the quota, a new reservation register is created.
|
||||||
|
|
||||||
|
This calculation method does not guarantee the quota enforcement if, for
|
||||||
|
example, the database isolation level is read committed; two transactions
|
||||||
|
can count the same number of resources and reservations, committing both
|
||||||
|
a new reservation exceeding the quota. But the goal of this reservation
|
||||||
|
method is to be fast enough to avoid the concurrency when counting the
|
||||||
|
resources while not blocking concurrent API operations.
|
||||||
|
"""
|
||||||
|
@db_api.retry_if_session_inactive()
|
||||||
|
def make_reservation(self, context, project_id, resources, deltas, plugin):
|
||||||
|
resources_over_limit = []
|
||||||
|
with db_api.CONTEXT_WRITER.using(context):
|
||||||
|
# Filter out unlimited resources.
|
||||||
|
limits = self.get_tenant_quotas(context, resources, project_id)
|
||||||
|
unlimited_resources = set([resource for (resource, limit) in
|
||||||
|
limits.items() if limit < 0])
|
||||||
|
requested_resources = (set(deltas.keys()) - unlimited_resources)
|
||||||
|
|
||||||
|
# Delete expired reservations before counting valid ones. This
|
||||||
|
# operation is fast and by calling it before making any
|
||||||
|
# reservation, we ensure the freshness of the reservations.
|
||||||
|
quota_api.remove_expired_reservations(context,
|
||||||
|
tenant_id=project_id)
|
||||||
|
|
||||||
|
# Count the number of (1) used and (2) reserved resources for this
|
||||||
|
# project_id. If any resource limit is exceeded, raise exception.
|
||||||
|
for resource_name in requested_resources:
|
||||||
|
tracked_resource = resources.get(resource_name)
|
||||||
|
if not tracked_resource:
|
||||||
|
continue
|
||||||
|
|
||||||
|
used_and_reserved = tracked_resource.count(
|
||||||
|
context, None, project_id, count_db_registers=True)
|
||||||
|
resource_num = deltas[resource_name]
|
||||||
|
if limits[resource_name] < (used_and_reserved + resource_num):
|
||||||
|
resources_over_limit.append(resource_name)
|
||||||
|
|
||||||
|
if resources_over_limit:
|
||||||
|
raise exceptions.OverQuota(overs=sorted(resources_over_limit))
|
||||||
|
|
||||||
|
return quota_api.create_reservation(context, project_id, deltas)
|
||||||
|
|
||||||
|
def cancel_reservation(self, context, reservation_id):
|
||||||
|
quota_api.remove_reservation(context, reservation_id, set_dirty=False)
|
@ -38,7 +38,7 @@ DEFAULT_QUOTAS_ACTION = 'default'
|
|||||||
RESOURCE_NAME = 'quota'
|
RESOURCE_NAME = 'quota'
|
||||||
RESOURCE_COLLECTION = RESOURCE_NAME + "s"
|
RESOURCE_COLLECTION = RESOURCE_NAME + "s"
|
||||||
QUOTAS = quota.QUOTAS
|
QUOTAS = quota.QUOTAS
|
||||||
DB_QUOTA_DRIVER = 'neutron.db.quota.driver.DbQuotaDriver'
|
DB_QUOTA_DRIVER = cfg.CONF.QUOTAS.quota_driver
|
||||||
EXTENDED_ATTRIBUTES_2_0 = {
|
EXTENDED_ATTRIBUTES_2_0 = {
|
||||||
RESOURCE_COLLECTION: {}
|
RESOURCE_COLLECTION: {}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ from oslo_config import cfg
|
|||||||
from neutron._i18n import _
|
from neutron._i18n import _
|
||||||
from neutron.api import extensions
|
from neutron.api import extensions
|
||||||
from neutron.api.v2 import resource
|
from neutron.api.v2 import resource
|
||||||
|
from neutron.db.quota import driver
|
||||||
|
from neutron.db.quota import driver_nolock
|
||||||
from neutron.extensions import quotasv2
|
from neutron.extensions import quotasv2
|
||||||
from neutron.quota import resource_registry
|
from neutron.quota import resource_registry
|
||||||
|
|
||||||
@ -32,7 +34,11 @@ RESOURCE_NAME = 'quota'
|
|||||||
ALIAS = RESOURCE_NAME + '_' + DETAIL_QUOTAS_ACTION
|
ALIAS = RESOURCE_NAME + '_' + DETAIL_QUOTAS_ACTION
|
||||||
QUOTA_DRIVER = cfg.CONF.QUOTAS.quota_driver
|
QUOTA_DRIVER = cfg.CONF.QUOTAS.quota_driver
|
||||||
RESOURCE_COLLECTION = RESOURCE_NAME + "s"
|
RESOURCE_COLLECTION = RESOURCE_NAME + "s"
|
||||||
DB_QUOTA_DRIVER = 'neutron.db.quota.driver.DbQuotaDriver'
|
DB_QUOTA_DRIVERS = tuple('.'.join([klass.__module__, klass.__name__])
|
||||||
|
for klass in (driver.DbQuotaDriver,
|
||||||
|
driver_nolock.DbQuotaNoLockDriver,
|
||||||
|
)
|
||||||
|
)
|
||||||
EXTENDED_ATTRIBUTES_2_0 = {
|
EXTENDED_ATTRIBUTES_2_0 = {
|
||||||
RESOURCE_COLLECTION: {}
|
RESOURCE_COLLECTION: {}
|
||||||
}
|
}
|
||||||
@ -61,8 +67,7 @@ class Quotasv2_detail(api_extensions.ExtensionDescriptor):
|
|||||||
|
|
||||||
# Ensure new extension is not loaded with old conf driver.
|
# Ensure new extension is not loaded with old conf driver.
|
||||||
extensions.register_custom_supported_check(
|
extensions.register_custom_supported_check(
|
||||||
ALIAS, lambda: QUOTA_DRIVER == DB_QUOTA_DRIVER,
|
ALIAS, lambda: QUOTA_DRIVER in DB_QUOTA_DRIVERS, plugin_agnostic=True)
|
||||||
plugin_agnostic=True)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_name(cls):
|
def get_name(cls):
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
from neutron_lib.db import api as db_api
|
from neutron_lib.db import api as db_api
|
||||||
from neutron_lib.plugins import constants
|
from neutron_lib.plugins import constants
|
||||||
from neutron_lib.plugins import directory
|
from neutron_lib.plugins import directory
|
||||||
@ -22,6 +24,8 @@ from sqlalchemy import exc as sql_exc
|
|||||||
from sqlalchemy.orm import session as se
|
from sqlalchemy.orm import session as se
|
||||||
|
|
||||||
from neutron._i18n import _
|
from neutron._i18n import _
|
||||||
|
from neutron.common import utils as n_utils
|
||||||
|
from neutron.conf import quota as quota_conf
|
||||||
from neutron.db.quota import api as quota_api
|
from neutron.db.quota import api as quota_api
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@ -55,7 +59,7 @@ def _count_resource(context, collection_name, tenant_id):
|
|||||||
_('No plugins that support counting %s found.') % collection_name)
|
_('No plugins that support counting %s found.') % collection_name)
|
||||||
|
|
||||||
|
|
||||||
class BaseResource(object):
|
class BaseResource(object, metaclass=abc.ABCMeta):
|
||||||
"""Describe a single resource for quota checking."""
|
"""Describe a single resource for quota checking."""
|
||||||
|
|
||||||
def __init__(self, name, flag, plural_name=None):
|
def __init__(self, name, flag, plural_name=None):
|
||||||
@ -98,6 +102,7 @@ class BaseResource(object):
|
|||||||
return max(value, -1)
|
return max(value, -1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
def dirty(self):
|
def dirty(self):
|
||||||
"""Return the current state of the Resource instance.
|
"""Return the current state of the Resource instance.
|
||||||
|
|
||||||
@ -106,6 +111,10 @@ class BaseResource(object):
|
|||||||
does not track usage.
|
does not track usage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def count(self, context, plugin, project_id, **kwargs):
|
||||||
|
"""Return the total count of this resource"""
|
||||||
|
|
||||||
|
|
||||||
class CountableResource(BaseResource):
|
class CountableResource(BaseResource):
|
||||||
"""Describe a resource where the counts are determined by a function."""
|
"""Describe a resource where the counts are determined by a function."""
|
||||||
@ -143,9 +152,13 @@ class CountableResource(BaseResource):
|
|||||||
name, flag=flag, plural_name=plural_name)
|
name, flag=flag, plural_name=plural_name)
|
||||||
self._count_func = count
|
self._count_func = count
|
||||||
|
|
||||||
def count(self, context, plugin, tenant_id, **kwargs):
|
@property
|
||||||
|
def dirty(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def count(self, context, plugin, project_id, **kwargs):
|
||||||
# NOTE(ihrachys) _count_resource doesn't receive plugin
|
# NOTE(ihrachys) _count_resource doesn't receive plugin
|
||||||
return self._count_func(context, self.plural_name, tenant_id)
|
return self._count_func(context, self.plural_name, project_id)
|
||||||
|
|
||||||
|
|
||||||
class TrackedResource(BaseResource):
|
class TrackedResource(BaseResource):
|
||||||
@ -184,13 +197,21 @@ class TrackedResource(BaseResource):
|
|||||||
self._model_class = model_class
|
self._model_class = model_class
|
||||||
self._dirty_tenants = set()
|
self._dirty_tenants = set()
|
||||||
self._out_of_sync_tenants = set()
|
self._out_of_sync_tenants = set()
|
||||||
|
# NOTE(ralonsoh): "DbQuotaNoLockDriver" driver does not need to track
|
||||||
|
# the DB events or resync the resource quota usage.
|
||||||
|
if cfg.CONF.QUOTAS.quota_driver == quota_conf.QUOTA_DB_DRIVER:
|
||||||
|
self._track_resource_events = False
|
||||||
|
else:
|
||||||
|
self._track_resource_events = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dirty(self):
|
def dirty(self):
|
||||||
|
if not self._track_resource_events:
|
||||||
|
return
|
||||||
return self._dirty_tenants
|
return self._dirty_tenants
|
||||||
|
|
||||||
def mark_dirty(self, context):
|
def mark_dirty(self, context):
|
||||||
if not self._dirty_tenants:
|
if not self._dirty_tenants or not self._track_resource_events:
|
||||||
return
|
return
|
||||||
with db_api.CONTEXT_WRITER.using(context):
|
with db_api.CONTEXT_WRITER.using(context):
|
||||||
# It is not necessary to protect this operation with a lock.
|
# It is not necessary to protect this operation with a lock.
|
||||||
@ -236,7 +257,8 @@ class TrackedResource(BaseResource):
|
|||||||
return usage_info
|
return usage_info
|
||||||
|
|
||||||
def resync(self, context, tenant_id):
|
def resync(self, context, tenant_id):
|
||||||
if tenant_id not in self._out_of_sync_tenants:
|
if (tenant_id not in self._out_of_sync_tenants or
|
||||||
|
not self._track_resource_events):
|
||||||
return
|
return
|
||||||
LOG.debug(("Synchronizing usage tracker for tenant:%(tenant_id)s on "
|
LOG.debug(("Synchronizing usage tracker for tenant:%(tenant_id)s on "
|
||||||
"resource:%(resource)s"),
|
"resource:%(resource)s"),
|
||||||
@ -302,15 +324,37 @@ class TrackedResource(BaseResource):
|
|||||||
reserved = reservations.get(self.name, 0)
|
reserved = reservations.get(self.name, 0)
|
||||||
return reserved
|
return reserved
|
||||||
|
|
||||||
def count(self, context, _plugin, tenant_id, resync_usage=True):
|
def count(self, context, _plugin, tenant_id, resync_usage=True,
|
||||||
|
count_db_registers=False):
|
||||||
"""Return the count of the resource.
|
"""Return the count of the resource.
|
||||||
|
|
||||||
The _plugin parameter is unused but kept for
|
The _plugin parameter is unused but kept for
|
||||||
compatibility with the signature of the count method for
|
compatibility with the signature of the count method for
|
||||||
CountableResource instances.
|
CountableResource instances.
|
||||||
"""
|
"""
|
||||||
return (self.count_used(context, tenant_id, resync_usage) +
|
if count_db_registers:
|
||||||
self.count_reserved(context, tenant_id))
|
count = self._count_db_registers(context, tenant_id)
|
||||||
|
else:
|
||||||
|
count = self.count_used(context, tenant_id, resync_usage)
|
||||||
|
|
||||||
|
return count + self.count_reserved(context, tenant_id)
|
||||||
|
|
||||||
|
def _count_db_registers(self, context, project_id):
|
||||||
|
"""Return the existing resources (self._model_class) in a project.
|
||||||
|
|
||||||
|
The query executed must be as fast as possible. To avoid retrieving all
|
||||||
|
model backref relationship columns, only "project_id" is requested
|
||||||
|
(this column always exists in the DB model because is used in the
|
||||||
|
filter).
|
||||||
|
"""
|
||||||
|
# TODO(ralonsoh): declare the OVO class instead the DB model and use
|
||||||
|
# ``NeutronDbObject.count`` with the needed filters and fields to
|
||||||
|
# retrieve ("project_id").
|
||||||
|
admin_context = n_utils.get_elevated_context(context)
|
||||||
|
with db_api.CONTEXT_READER.using(admin_context):
|
||||||
|
query = admin_context.session.query(self._model_class.project_id)
|
||||||
|
query = query.filter(self._model_class.project_id == project_id)
|
||||||
|
return query.count()
|
||||||
|
|
||||||
def _except_bulk_delete(self, delete_context):
|
def _except_bulk_delete(self, delete_context):
|
||||||
if delete_context.mapper.class_ == self._model_class:
|
if delete_context.mapper.class_ == self._model_class:
|
||||||
@ -321,12 +365,16 @@ class TrackedResource(BaseResource):
|
|||||||
self._model_class)
|
self._model_class)
|
||||||
|
|
||||||
def register_events(self):
|
def register_events(self):
|
||||||
|
if not self._track_resource_events:
|
||||||
|
return
|
||||||
listen = db_api.sqla_listen
|
listen = db_api.sqla_listen
|
||||||
listen(self._model_class, 'after_insert', self._db_event_handler)
|
listen(self._model_class, 'after_insert', self._db_event_handler)
|
||||||
listen(self._model_class, 'after_delete', self._db_event_handler)
|
listen(self._model_class, 'after_delete', self._db_event_handler)
|
||||||
listen(se.Session, 'after_bulk_delete', self._except_bulk_delete)
|
listen(se.Session, 'after_bulk_delete', self._except_bulk_delete)
|
||||||
|
|
||||||
def unregister_events(self):
|
def unregister_events(self):
|
||||||
|
if not self._track_resource_events:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
db_api.sqla_remove(self._model_class, 'after_insert',
|
db_api.sqla_remove(self._model_class, 'after_insert',
|
||||||
self._db_event_handler)
|
self._db_event_handler)
|
||||||
|
@ -51,6 +51,7 @@ from neutron.common import ipv6_utils
|
|||||||
from neutron.common import test_lib
|
from neutron.common import test_lib
|
||||||
from neutron.common import utils
|
from neutron.common import utils
|
||||||
from neutron.conf import policies
|
from neutron.conf import policies
|
||||||
|
from neutron.conf import quota as quota_conf
|
||||||
from neutron.db import db_base_plugin_common
|
from neutron.db import db_base_plugin_common
|
||||||
from neutron.db import ipam_backend_mixin
|
from neutron.db import ipam_backend_mixin
|
||||||
from neutron.db.models import l3 as l3_models
|
from neutron.db.models import l3 as l3_models
|
||||||
@ -110,7 +111,10 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
|||||||
|
|
||||||
def setUp(self, plugin=None, service_plugins=None,
|
def setUp(self, plugin=None, service_plugins=None,
|
||||||
ext_mgr=None):
|
ext_mgr=None):
|
||||||
|
quota_conf.register_quota_opts(quota_conf.core_quota_opts, cfg.CONF)
|
||||||
|
cfg.CONF.set_override(
|
||||||
|
'quota_driver', 'neutron.db.quota.driver.DbQuotaDriver',
|
||||||
|
group=quota_conf.QUOTAS_CFG_GROUP)
|
||||||
super(NeutronDbPluginV2TestCase, self).setUp()
|
super(NeutronDbPluginV2TestCase, self).setUp()
|
||||||
cfg.CONF.set_override('notify_nova_on_port_status_changes', False)
|
cfg.CONF.set_override('notify_nova_on_port_status_changes', False)
|
||||||
cfg.CONF.set_override('allow_overlapping_ips', True)
|
cfg.CONF.set_override('allow_overlapping_ips', True)
|
||||||
|
@ -30,6 +30,7 @@ from neutron.api.v2 import router
|
|||||||
from neutron.common import config
|
from neutron.common import config
|
||||||
from neutron.conf import quota as qconf
|
from neutron.conf import quota as qconf
|
||||||
from neutron.db.quota import driver
|
from neutron.db.quota import driver
|
||||||
|
from neutron.db.quota import driver_nolock
|
||||||
from neutron import quota
|
from neutron import quota
|
||||||
from neutron.quota import resource_registry
|
from neutron.quota import resource_registry
|
||||||
from neutron.tests import base
|
from neutron.tests import base
|
||||||
@ -504,29 +505,24 @@ class TestDbQuotaDriver(base.BaseTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestQuotaDriverLoad(base.BaseTestCase):
|
class TestQuotaDriverLoad(base.BaseTestCase):
|
||||||
def setUp(self):
|
|
||||||
super(TestQuotaDriverLoad, self).setUp()
|
|
||||||
# Make sure QuotaEngine is reinitialized in each test.
|
|
||||||
quota.QUOTAS._driver = None
|
|
||||||
|
|
||||||
def _test_quota_driver(self, cfg_driver, loaded_driver,
|
def _test_quota_driver(self, cfg_driver, loaded_driver,
|
||||||
with_quota_db_module=True):
|
with_quota_db_module=True):
|
||||||
|
quota.QUOTAS._driver = None
|
||||||
cfg.CONF.set_override('quota_driver', cfg_driver, group='QUOTAS')
|
cfg.CONF.set_override('quota_driver', cfg_driver, group='QUOTAS')
|
||||||
with mock.patch.dict(sys.modules, {}):
|
with mock.patch.dict(sys.modules, {}):
|
||||||
if (not with_quota_db_module and
|
if (not with_quota_db_module and
|
||||||
'neutron.db.quota.driver' in sys.modules):
|
quota.QUOTA_DB_MODULE in sys.modules):
|
||||||
del sys.modules['neutron.db.quota.driver']
|
del sys.modules[quota.QUOTA_DB_MODULE]
|
||||||
driver = quota.QUOTAS.get_driver()
|
driver = quota.QUOTAS.get_driver()
|
||||||
self.assertEqual(loaded_driver, driver.__class__.__name__)
|
self.assertEqual(loaded_driver, driver.__class__.__name__)
|
||||||
|
|
||||||
def test_quota_db_driver_with_quotas_table(self):
|
def test_quota_driver_load(self):
|
||||||
self._test_quota_driver('neutron.db.quota.driver.DbQuotaDriver',
|
for klass in (quota.ConfDriver, driver.DbQuotaDriver,
|
||||||
'DbQuotaDriver', True)
|
driver_nolock.DbQuotaNoLockDriver):
|
||||||
|
self._test_quota_driver(
|
||||||
|
'.'.join([klass.__module__, klass.__name__]),
|
||||||
|
klass.__name__, True)
|
||||||
|
|
||||||
def test_quota_db_driver_fallback_conf_driver(self):
|
def test_quota_driver_fallback_conf_driver(self):
|
||||||
self._test_quota_driver('neutron.db.quota.driver.DbQuotaDriver',
|
self._test_quota_driver(quota.QUOTA_DB_DRIVER, 'ConfDriver', False)
|
||||||
'ConfDriver', False)
|
|
||||||
|
|
||||||
def test_quota_conf_driver(self):
|
|
||||||
self._test_quota_driver('neutron.quota.ConfDriver',
|
|
||||||
'ConfDriver', True)
|
|
||||||
|
@ -30,34 +30,44 @@ from neutron.tests.unit import testlib_api
|
|||||||
|
|
||||||
|
|
||||||
DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
|
DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
|
||||||
|
QUOTA_DRIVER = 'neutron.db.quota.DbQuotaDriver'
|
||||||
|
|
||||||
|
|
||||||
meh_quota_flag = 'quota_meh'
|
meh_quota_flag = 'quota_meh'
|
||||||
meh_quota_opts = [cfg.IntOpt(meh_quota_flag, default=99)]
|
meh_quota_opts = [cfg.IntOpt(meh_quota_flag, default=99)]
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseResource(resource.BaseResource):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dirty(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def count(self, context, plugin, project_id, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestResource(base.DietTestCase):
|
class TestResource(base.DietTestCase):
|
||||||
"""Unit tests for neutron.quota.resource.BaseResource"""
|
"""Unit tests for neutron.quota.resource.BaseResource"""
|
||||||
|
|
||||||
def test_create_resource_without_plural_name(self):
|
def test_create_resource_without_plural_name(self):
|
||||||
res = resource.BaseResource('foo', None)
|
res = _BaseResource('foo', None)
|
||||||
self.assertEqual('foos', res.plural_name)
|
self.assertEqual('foos', res.plural_name)
|
||||||
res = resource.BaseResource('foy', None)
|
res = _BaseResource('foy', None)
|
||||||
self.assertEqual('foies', res.plural_name)
|
self.assertEqual('foies', res.plural_name)
|
||||||
|
|
||||||
def test_create_resource_with_plural_name(self):
|
def test_create_resource_with_plural_name(self):
|
||||||
res = resource.BaseResource('foo', None,
|
res = _BaseResource('foo', None, plural_name='foopsies')
|
||||||
plural_name='foopsies')
|
|
||||||
self.assertEqual('foopsies', res.plural_name)
|
self.assertEqual('foopsies', res.plural_name)
|
||||||
|
|
||||||
def test_resource_default_value(self):
|
def test_resource_default_value(self):
|
||||||
res = resource.BaseResource('foo', 'foo_quota')
|
res = _BaseResource('foo', 'foo_quota')
|
||||||
with mock.patch('oslo_config.cfg.CONF') as mock_cfg:
|
with mock.patch('oslo_config.cfg.CONF') as mock_cfg:
|
||||||
mock_cfg.QUOTAS.foo_quota = 99
|
mock_cfg.QUOTAS.foo_quota = 99
|
||||||
self.assertEqual(99, res.default)
|
self.assertEqual(99, res.default)
|
||||||
|
|
||||||
def test_resource_negative_default_value(self):
|
def test_resource_negative_default_value(self):
|
||||||
res = resource.BaseResource('foo', 'foo_quota')
|
res = _BaseResource('foo', 'foo_quota')
|
||||||
with mock.patch('oslo_config.cfg.CONF') as mock_cfg:
|
with mock.patch('oslo_config.cfg.CONF') as mock_cfg:
|
||||||
mock_cfg.QUOTAS.foo_quota = -99
|
mock_cfg.QUOTAS.foo_quota = -99
|
||||||
self.assertEqual(-1, res.default)
|
self.assertEqual(-1, res.default)
|
||||||
@ -94,6 +104,7 @@ class TestTrackedResource(testlib_api.SqlTestCase):
|
|||||||
session.add(item)
|
session.add(item)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
cfg.CONF.set_override('quota_driver', QUOTA_DRIVER, group='QUOTAS')
|
||||||
super(TestTrackedResource, self).setUp()
|
super(TestTrackedResource, self).setUp()
|
||||||
self.setup_coreplugin(DB_PLUGIN_KLASS)
|
self.setup_coreplugin(DB_PLUGIN_KLASS)
|
||||||
self.resource = 'meh'
|
self.resource = 'meh'
|
||||||
|
@ -160,6 +160,7 @@ class TestAuxiliaryFunctions(base.DietTestCase):
|
|||||||
'TrackedResource.mark_dirty') as mock_mark_dirty:
|
'TrackedResource.mark_dirty') as mock_mark_dirty:
|
||||||
self.registry.set_tracked_resource('meh', test_quota.MehModel)
|
self.registry.set_tracked_resource('meh', test_quota.MehModel)
|
||||||
self.registry.register_resource_by_name('meh')
|
self.registry.register_resource_by_name('meh')
|
||||||
|
self.registry.resources['meh']._track_resource_events = True
|
||||||
res = self.registry.get_resource('meh')
|
res = self.registry.get_resource('meh')
|
||||||
# This ensures dirty is true
|
# This ensures dirty is true
|
||||||
res._dirty_tenants.add('tenant_id')
|
res._dirty_tenants.add('tenant_id')
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
A new quota driver is added: ``DbQuotaNoLockDriver``. This driver, unlike
|
||||||
|
``DbQuotaDriver``, does not create a unique lock per (resource,
|
||||||
|
project_id). That may lead to a database deadlock state if the number of
|
||||||
|
server requests exceeds the number of resolved resource creations, as
|
||||||
|
described in `LP#1926787 <https://bugs.launchpad.net/neutron/+bug/1926787>`_.
|
||||||
|
This driver relays on the database transactionality isolation and counts
|
||||||
|
the number of used and reserved resources and, if available, creates the
|
||||||
|
new resource reservations in one single database transaction.
|
Loading…
Reference in New Issue
Block a user