Merge "Prevent quota and reservations to go into negative"

This commit is contained in:
Zuul 2021-04-02 08:09:19 +00:00 committed by Gerrit Code Review
commit c14aa05725
2 changed files with 105 additions and 11 deletions

View File

@ -1289,8 +1289,9 @@ def quota_reserve(context, resources, quotas, deltas, expire,
usages[resource].reserved += delta usages[resource].reserved += delta
if unders: if unders:
LOG.warning("Change will make usage less than 0 for the following " LOG.warning("Reservation would make usage less than 0 for the "
"resources: %s", unders) "following resources, so on commit they will be "
"limited to prevent going below 0: %s", unders)
if overs: if overs:
usages = {k: dict(in_use=v.in_use, reserved=v.reserved) usages = {k: dict(in_use=v.in_use, reserved=v.reserved)
for k, v in usages.items()} for k, v in usages.items()}
@ -1345,9 +1346,14 @@ def reservation_commit(context, reservations, project_id=None):
for reservation in _quota_reservations(session, context, reservations): for reservation in _quota_reservations(session, context, reservations):
usage = usages[reservation.usage_id] usage = usages[reservation.usage_id]
if reservation.delta >= 0: delta = reservation.delta
usage.reserved -= reservation.delta if delta >= 0:
usage.in_use += reservation.delta usage.reserved -= min(delta, usage.reserved)
# For negative deltas make sure we never go into negative usage
elif -delta > usage.in_use:
delta = -usage.in_use
usage.in_use += delta
reservation.delete(session=session) reservation.delete(session=session)
@ -1365,7 +1371,7 @@ def reservation_rollback(context, reservations, project_id=None):
for reservation in _quota_reservations(session, context, reservations): for reservation in _quota_reservations(session, context, reservations):
usage = usages[reservation.usage_id] usage = usages[reservation.usage_id]
if reservation.delta >= 0: if reservation.delta >= 0:
usage.reserved -= reservation.delta usage.reserved -= min(reservation.delta, usage.reserved)
reservation.delete(session=session) reservation.delete(session=session)
@ -1434,7 +1440,8 @@ def reservation_expire(context):
if results: if results:
for reservation in results: for reservation in results:
if reservation.delta >= 0: if reservation.delta >= 0:
reservation.usage.reserved -= reservation.delta reservation.usage.reserved -= min(
reservation.delta, reservation.usage.reserved)
reservation.usage.save(session=session) reservation.usage.save(session=session)
reservation.delete(session=session) reservation.delete(session=session)

View File

@ -45,7 +45,7 @@ ONE_HUNDREDS = 100
UTC_NOW = timeutils.utcnow() UTC_NOW = timeutils.utcnow()
def _quota_reserve(context, project_id): def _quota_reserve(context, project_id, **resource_dict):
"""Create sample Quota, QuotaUsage and Reservation objects. """Create sample Quota, QuotaUsage and Reservation objects.
There is no method db.quota_usage_create(), so we have to use There is no method db.quota_usage_create(), so we have to use
@ -58,15 +58,19 @@ def _quota_reserve(context, project_id):
def sync(elevated, project_id, session): def sync(elevated, project_id, session):
return {resource: usage} return {resource: usage}
return sync return sync
if not resource_dict:
resource_dict = {'volumes': 1, 'gigabytes': 2}
quotas = {} quotas = {}
resources = {} resources = {}
deltas = {} deltas = {}
for i, resource in enumerate(('volumes', 'gigabytes')): for resource, value in resource_dict.items():
quota_obj = db.quota_create(context, project_id, resource, i + 1) quota_obj = db.quota_create(context, project_id, resource, value)
quotas[resource] = quota_obj.hard_limit quotas[resource] = quota_obj.hard_limit
resources[resource] = quota.ReservableResource(resource, resources[resource] = quota.ReservableResource(resource,
'_sync_%s' % resource) '_sync_%s' % resource)
deltas[resource] = i + 1 deltas[resource] = value
return db.quota_reserve( return db.quota_reserve(
context, resources, quotas, deltas, context, resources, quotas, deltas,
datetime.datetime.utcnow(), until_refresh=None, datetime.datetime.utcnow(), until_refresh=None,
@ -2530,6 +2534,47 @@ class DBAPIReservationTestCase(BaseTest):
self.ctxt, self.ctxt,
'project1')) 'project1'))
def test_reservation_commit_negative_reservation(self):
"""Verify we can't make reservations negative on commit."""
project = 'project1'
reservations = _quota_reserve(self.ctxt, project, volumes=2)
# Force a smaller reserved value in quota_usages table
session = sqlalchemy_api.get_session()
with session.begin():
vol_usage = db.quota_usage_get(self.ctxt, project, 'volumes')
vol_usage.reserved -= 1
vol_usage.save(session=session)
# When committing 2 volumes from reserved to used reserved should not
# go from 1 to -1 but from 1 to 0, but in-use should still increase by
# 2
db.reservation_commit(self.ctxt, reservations, project)
expected = {'project_id': project,
'volumes': {'reserved': 0, 'in_use': 2}}
self.assertEqual(expected,
db.quota_usage_get_all_by_project(self.ctxt, project))
def test_reservation_commit_negative_in_use(self):
"""Verify we can't make in-use negative on commit."""
project = 'project1'
reservations = _quota_reserve(self.ctxt, project, volumes=-2)
# Force a smaller in_use than the one the reservation will decrease
session = sqlalchemy_api.get_session()
with session.begin():
vol_usage = db.quota_usage_get(self.ctxt, 'project1', 'volumes')
vol_usage.in_use = 1
vol_usage.save(session=session)
# When committing -2 volumes from reserved to in-use they should not
# make in-use go from 1 to -1, but from 1 to 0
db.reservation_commit(self.ctxt, reservations, project)
expected = {'project_id': project,
'volumes': {'reserved': 0, 'in_use': 0}}
self.assertEqual(expected,
db.quota_usage_get_all_by_project(self.ctxt, project))
def test_reservation_rollback(self): def test_reservation_rollback(self):
reservations = _quota_reserve(self.ctxt, 'project1') reservations = _quota_reserve(self.ctxt, 'project1')
expected = {'project_id': 'project1', expected = {'project_id': 'project1',
@ -2550,6 +2595,27 @@ class DBAPIReservationTestCase(BaseTest):
self.ctxt, self.ctxt,
'project1')) 'project1'))
def test_reservation_rollback_negative(self):
"""Verify we can't make reservations negative on rollback."""
project = 'project1'
reservations = _quota_reserve(self.ctxt, project, volumes=2)
# Force a smaller reserved value in quota_usages table
session = sqlalchemy_api.get_session()
with session.begin():
vol_usage = db.quota_usage_get(self.ctxt, project, 'volumes')
vol_usage.reserved -= 1
vol_usage.save(session=session)
# When rolling back 2 volumes from reserved when there's only 1 in the
# quota usage's reserved field, reserved should not go from 1 to -1
# but from 1 to 0
db.reservation_rollback(self.ctxt, reservations, project)
expected = {'project_id': project,
'volumes': {'reserved': 0, 'in_use': 0}}
self.assertEqual(expected,
db.quota_usage_get_all_by_project(self.ctxt, project))
def test_reservation_expire(self): def test_reservation_expire(self):
self.values['expire'] = datetime.datetime.utcnow() + \ self.values['expire'] = datetime.datetime.utcnow() + \
datetime.timedelta(days=1) datetime.timedelta(days=1)
@ -2564,6 +2630,27 @@ class DBAPIReservationTestCase(BaseTest):
self.ctxt, self.ctxt,
'project1')) 'project1'))
def test_reservation_expire_negative(self):
"""Verify we can't make reservation negative on expiration."""
project = 'project1'
_quota_reserve(self.ctxt, project, volumes=2)
# Force a smaller reserved value in quota_usages table
session = sqlalchemy_api.get_session()
with session.begin():
vol_usage = db.quota_usage_get(self.ctxt, project, 'volumes')
vol_usage.reserved -= 1
vol_usage.save(session=session)
# When expiring 2 volumes from reserved when there's only 1 in the
# quota usage's reserved field, reserved should not go from 1 to -1
# but from 1 to 0
db.reservation_expire(self.ctxt)
expected = {'project_id': project,
'volumes': {'reserved': 0, 'in_use': 0}}
self.assertEqual(expected,
db.quota_usage_get_all_by_project(self.ctxt, project))
@mock.patch('time.sleep', mock.Mock()) @mock.patch('time.sleep', mock.Mock())
def test_quota_reserve_create_usages_race(self): def test_quota_reserve_create_usages_race(self):
"""Test we retry when there is a race in creation.""" """Test we retry when there is a race in creation."""