Skip to content

Commit 799c0e4

Browse files
committed
Use instance mappings to count server group members
This adds a get_count_by_uuids_and_user() method to the InstanceMapping object and uses it to count instance mappings for the purpose of counting quota usage for server group members. By counting server group members via instance mappings, the count is resilient to down cells in a multi-cell environment. Part of blueprint count-quota-usage-from-placement Change-Id: I3ff39d5ed99a68ad8678e5ff62b343f3018b4768
1 parent f17dbe6 commit 799c0e4

File tree

5 files changed

+153
-8
lines changed

5 files changed

+153
-8
lines changed

nova/objects/instance_mapping.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,3 +432,28 @@ def get_counts(cls, context, project_id, user_id=None):
432432
'user': {'instances': <count across user>}}
433433
"""
434434
return cls._get_counts_in_db(context, project_id, user_id=user_id)
435+
436+
@staticmethod
437+
@db_api.api_context_manager.reader
438+
def _get_count_by_uuids_and_user_in_db(context, uuids, user_id):
439+
query = (context.session.query(
440+
func.count(api_models.InstanceMapping.id))
441+
.filter(api_models.InstanceMapping.instance_uuid.in_(uuids))
442+
.filter_by(queued_for_delete=False)
443+
.filter_by(user_id=user_id))
444+
return query.scalar()
445+
446+
@classmethod
447+
def get_count_by_uuids_and_user(cls, context, uuids, user_id):
448+
"""Get the count of InstanceMapping objects by UUIDs and user_id.
449+
450+
The count is used to represent the count of server group members
451+
belonging to a particular user, for the purpose of counting quota
452+
usage. Instances that are queued_for_deleted=True are not included in
453+
the count (deleted and SOFT_DELETED instances).
454+
455+
:param uuids: List of instance UUIDs on which to filter
456+
:param user_id: The user_id on which to filter
457+
:returns: An integer for the count
458+
"""
459+
return cls._get_count_by_uuids_and_user_in_db(context, uuids, user_id)

nova/quota.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
# If user_id and queued_for_delete are populated for a project, cache the
4444
# result to avoid doing unnecessary EXISTS database queries.
4545
UID_QFD_POPULATED_CACHE_BY_PROJECT = set()
46+
# For the server group members check, we do not scope to a project, so if all
47+
# user_id and queued_for_delete are populated for all projects, cache the
48+
# result to avoid doing unnecessary EXISTS database queries.
49+
UID_QFD_POPULATED_CACHE_ALL = False
4650

4751

4852
class DbQuotaDriver(object):
@@ -1126,15 +1130,13 @@ def _security_group_count(context, project_id, user_id=None):
11261130
user_id=user_id)
11271131

11281132

1129-
def _server_group_count_members_by_user(context, group, user_id):
1133+
def _server_group_count_members_by_user_legacy(context, group, user_id):
11301134
# NOTE(melwitt): This is mostly duplicated from
11311135
# InstanceGroup.count_members_by_user() to query across multiple cells.
11321136
# We need to be able to pass the correct cell context to
11331137
# InstanceList.get_by_filters().
1134-
# TODO(melwitt): Counting across cells for instances means we will miss
1135-
# counting resources if a cell is down. In the future, we should query
1136-
# placement for cores/ram and InstanceMappings for instances (once we are
1137-
# deleting InstanceMappings when we delete instances).
1138+
# NOTE(melwitt): Counting across cells for instances means we will miss
1139+
# counting resources if a cell is down.
11381140
cell_mappings = objects.CellMappingList.get_all(context)
11391141
greenthreads = []
11401142
filters = {'deleted': False, 'user_id': user_id, 'uuid': group.members}
@@ -1163,6 +1165,41 @@ def _server_group_count_members_by_user(context, group, user_id):
11631165
return {'user': {'server_group_members': count}}
11641166

11651167

1168+
def _server_group_count_members_by_user(context, group, user_id):
1169+
"""Get the count of server group members for a group by user.
1170+
1171+
:param context: The request context for database access
1172+
:param group: The InstanceGroup object with members to count
1173+
:param user_id: The user_id to count across
1174+
:returns: A dict containing the user-scoped count. For example:
1175+
1176+
{'user': 'server_group_members': <count across user>}}
1177+
"""
1178+
# Because server group members quota counting is not scoped to a project,
1179+
# but scoped to a particular InstanceGroup and user, we cannot filter our
1180+
# user_id/queued_for_delete populated check on project_id or user_id.
1181+
# So, we check whether user_id/queued_for_delete is populated for all
1182+
# records and cache the result to prevent unnecessary checking once the
1183+
# data migration has been completed.
1184+
global UID_QFD_POPULATED_CACHE_ALL
1185+
if not UID_QFD_POPULATED_CACHE_ALL:
1186+
LOG.debug('Checking whether user_id and queued_for_delete are '
1187+
'populated for all projects')
1188+
uid_qfd_populated = _user_id_queued_for_delete_populated(context)
1189+
if uid_qfd_populated:
1190+
UID_QFD_POPULATED_CACHE_ALL = True
1191+
1192+
if UID_QFD_POPULATED_CACHE_ALL:
1193+
count = objects.InstanceMappingList.get_count_by_uuids_and_user(
1194+
context, group.members, user_id)
1195+
return {'user': {'server_group_members': count}}
1196+
1197+
LOG.warning('Falling back to legacy quota counting method for server '
1198+
'group members')
1199+
return _server_group_count_members_by_user_legacy(context, group,
1200+
user_id)
1201+
1202+
11661203
def _fixed_ip_count(context, project_id):
11671204
# NOTE(melwitt): This assumes a single cell.
11681205
count = objects.FixedIPList.get_count_by_project(context, project_id)
@@ -1261,7 +1298,8 @@ def _instances_cores_ram_count(context, project_id, user_id=None):
12611298
if CONF.quota.count_usage_from_placement:
12621299
# If a project has all user_id and queued_for_delete data populated,
12631300
# cache the result to avoid needless database checking in the future.
1264-
if project_id not in UID_QFD_POPULATED_CACHE_BY_PROJECT:
1301+
if (not UID_QFD_POPULATED_CACHE_ALL and
1302+
project_id not in UID_QFD_POPULATED_CACHE_BY_PROJECT):
12651303
LOG.debug('Checking whether user_id and queued_for_delete are '
12661304
'populated for project_id %s', project_id)
12671305
uid_qfd_populated = _user_id_queued_for_delete_populated(

nova/test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ def setUp(self):
308308

309309
# NOTE(melwitt): Reset the cached set of projects
310310
quota.UID_QFD_POPULATED_CACHE_BY_PROJECT = set()
311+
quota.UID_QFD_POPULATED_CACHE_ALL = False
311312

312313
def _setup_cells(self):
313314
"""Setup a normal cellsv2 environment.

nova/tests/functional/db/test_quota.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
# License for the specific language governing permissions and limitations
1111
# under the License.
1212

13+
import ddt
14+
import mock
1315
from oslo_utils import uuidutils
1416

1517
from nova import context
@@ -20,6 +22,7 @@
2022
from nova.tests.functional.db import test_instance_mapping
2123

2224

25+
@ddt.ddt
2326
class QuotaTestCase(test.NoDBTestCase):
2427
USES_DB_SELF = True
2528

@@ -32,7 +35,13 @@ def setUp(self):
3235
fix.add_cell_database('cell2')
3336
self.useFixture(fix)
3437

35-
def test_server_group_members_count_by_user(self):
38+
@ddt.data(True, False)
39+
@mock.patch('nova.quota.LOG.warning')
40+
@mock.patch('nova.quota._user_id_queued_for_delete_populated')
41+
def test_server_group_members_count_by_user(self, uid_qfd_populated,
42+
mock_uid_qfd_populated,
43+
mock_warn_log):
44+
mock_uid_qfd_populated.return_value = uid_qfd_populated
3645
ctxt = context.RequestContext('fake-user', 'fake-project')
3746
mapping1 = objects.CellMapping(context=ctxt,
3847
uuid=uuidutils.generate_uuid(),
@@ -59,6 +68,12 @@ def test_server_group_members_count_by_user(self):
5968
user_id='fake-user')
6069
instance.create()
6170
instance_uuids.append(instance.uuid)
71+
im = objects.InstanceMapping(context=ctxt,
72+
instance_uuid=instance.uuid,
73+
project_id='fake-project',
74+
user_id='fake-user',
75+
cell_id=mapping1.id)
76+
im.create()
6277

6378
# Create an instance in cell2
6479
with context.target_cell(ctxt, mapping2) as cctxt:
@@ -67,18 +82,50 @@ def test_server_group_members_count_by_user(self):
6782
user_id='fake-user')
6883
instance.create()
6984
instance_uuids.append(instance.uuid)
85+
im = objects.InstanceMapping(context=ctxt,
86+
instance_uuid=instance.uuid,
87+
project_id='fake-project',
88+
user_id='fake-user',
89+
cell_id=mapping2.id)
90+
im.create()
91+
92+
# Create an instance that is queued for delete in cell2. It should not
93+
# be counted
94+
with context.target_cell(ctxt, mapping2) as cctxt:
95+
instance = objects.Instance(context=cctxt,
96+
project_id='fake-project',
97+
user_id='fake-user')
98+
instance.create()
99+
instance.destroy()
100+
instance_uuids.append(instance.uuid)
101+
im = objects.InstanceMapping(context=ctxt,
102+
instance_uuid=instance.uuid,
103+
project_id='fake-project',
104+
user_id='fake-user',
105+
cell_id=mapping2.id,
106+
queued_for_delete=True)
107+
im.create()
70108

71109
# Add the uuids to the group
72110
objects.InstanceGroup.add_members(ctxt, group.uuid, instance_uuids)
73111
# add_members() doesn't add the members to the object field
74112
group.members.extend(instance_uuids)
75113

76-
# Count server group members across cells
114+
# Count server group members from instance mappings or cell databases,
115+
# depending on whether the user_id/queued_for_delete data migration has
116+
# been completed.
77117
count = quota._server_group_count_members_by_user(ctxt, group,
78118
'fake-user')
79119

80120
self.assertEqual(2, count['user']['server_group_members'])
81121

122+
if uid_qfd_populated:
123+
# Did not log a warning about falling back to legacy count.
124+
mock_warn_log.assert_not_called()
125+
else:
126+
# Logged a warning about falling back to legacy count.
127+
mock_warn_log.assert_called_once()
128+
82129
def test_instances_cores_ram_count(self):
83130
ctxt = context.RequestContext('fake-user', 'fake-project')
84131
mapping1 = objects.CellMapping(context=ctxt,

nova/tests/unit/test_quota.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,3 +2112,37 @@ def test_user_id_queued_for_delete_populated_cache_by_project(
21122112
mock.sentinel.project_id,
21132113
user_id=mock.sentinel.user_id)
21142114
mock_uid_qfd_populated.assert_not_called()
2115+
2116+
@mock.patch('nova.quota._user_id_queued_for_delete_populated')
2117+
@mock.patch('nova.quota._server_group_count_members_by_user_legacy')
2118+
@mock.patch('nova.objects.InstanceMappingList.get_count_by_uuids_and_user')
2119+
@mock.patch('nova.quota._instances_cores_ram_count_legacy')
2120+
@mock.patch('nova.quota._instances_cores_ram_count_api_db_placement')
2121+
def test_user_id_queued_for_delete_populated_cache_all(
2122+
self, mock_api_db_placement_count, mock_legacy_icr_count,
2123+
mock_api_db_sgm_count, mock_legacy_sgm_count,
2124+
mock_uid_qfd_populated):
2125+
# Check the case where the data migration was found to be complete by a
2126+
# server group members count not scoped to a project.
2127+
mock_uid_qfd_populated.return_value = True
2128+
# Server group members call will check whether there are any unmigrated
2129+
# records.
2130+
fake_group = mock.Mock()
2131+
quota._server_group_count_members_by_user(mock.sentinel.context,
2132+
fake_group,
2133+
mock.sentinel.user_id)
2134+
mock_uid_qfd_populated.assert_called_once()
2135+
# Second server group members call should skip the check for user_id
2136+
# and queued_for_delete migrated because the result was cached.
2137+
mock_uid_qfd_populated.reset_mock()
2138+
quota._server_group_count_members_by_user(mock.sentinel.context,
2139+
fake_group,
2140+
mock.sentinel.user_id)
2141+
mock_uid_qfd_populated.assert_not_called()
2142+
# A call to count instances, cores, and ram should skip the check for
2143+
# user_id and queued_for_delete migrated because the result was cached
2144+
# during the call to count server group members.
2145+
mock_uid_qfd_populated.reset_mock()
2146+
quota._instances_cores_ram_count(mock.sentinel.context,
2147+
mock.sentinel.project_id)
2148+
mock_uid_qfd_populated.assert_not_called()

0 commit comments

Comments
 (0)