Make API calls in Instances view parallel

In order to increase rendering speed, make
api.nova.server_list, api.network.servers_update_addresses,
api.nova.flavor_list and api.glance.image_list_detailed
parallel. For admin panel also api.keystone.tenant_list
is parallelized.

Closes-bug: #1655307

Change-Id: I83150d3963f1233edc9efbcdac299520dedbed3c
This commit is contained in:
Mateusz Kowalski 2017-01-28 11:52:06 +01:00
parent d07a02a60c
commit df194c8b4c
3 changed files with 154 additions and 115 deletions

View File

@ -140,9 +140,9 @@ class InstanceViewTest(test.BaseAdminViewTests):
instances = res.context['table'].data
self.assertTemplateUsed(res, INDEX_TEMPLATE)
# Since error messages produced for each instance are identical,
# there will be only one error message for all instances
# there will be only two error messages for all instances
# (messages de-duplication).
self.assertMessageCount(res, error=1)
self.assertMessageCount(res, error=2)
self.assertItemsEqual(instances, servers)
@test.create_stubs({

View File

@ -17,8 +17,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from collections import OrderedDict
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
@ -39,6 +37,8 @@ from openstack_dashboard.dashboards.project.instances import views
from openstack_dashboard.dashboards.project.instances.workflows \
import update_instance
import futurist
# re-use console from project.instances.views to make reflection work
def console(args, **kvargs):
@ -82,6 +82,12 @@ class AdminIndexView(tables.DataTableView):
def get_data(self):
instances = []
tenants = []
tenant_dict = {}
images = []
flavors = []
full_flavors = {}
marker = self.request.GET.get(
project_tables.AdminInstancesTable._meta.pagination_param, None)
default_search_opts = {'marker': marker, 'paginate': True}
@ -99,27 +105,72 @@ class AdminIndexView(tables.DataTableView):
return instances
self._needs_filter_first = False
# Gather our tenants to correlate against IDs
try:
tenants, has_more = api.keystone.tenant_list(self.request)
except Exception:
tenants = []
msg = _('Unable to retrieve instance project information.')
exceptions.handle(self.request, msg)
# Gather our images to correlate againts IDs
try:
images = api.glance.image_list_detailed(self.request)[0]
except Exception:
images = []
msg = _("Unable to retrieve image list.")
def _task_get_tenants():
# Gather our tenants to correlate against IDs
try:
tmp_tenants, __ = api.keystone.tenant_list(self.request)
tenants.extend(tmp_tenants)
tenant_dict.update([(t.id, t) for t in tenants])
except Exception:
msg = _('Unable to retrieve instance project information.')
exceptions.handle(self.request, msg)
# Gather our flavors to correlate against IDs
try:
flavors = api.nova.flavor_list(self.request)
except Exception:
# If fails to retrieve flavor list, creates an empty list.
flavors = []
def _task_get_images():
# Gather our images to correlate againts IDs
try:
tmp_images = api.glance.image_list_detailed(self.request)[0]
images.extend(tmp_images)
except Exception:
msg = _("Unable to retrieve image list.")
exceptions.handle(self.request, msg)
def _task_get_flavors():
# Gather our flavors to correlate against IDs
try:
tmp_flavors = api.nova.flavor_list(self.request)
flavors.extend(tmp_flavors)
full_flavors.update([(str(flavor.id), flavor)
for flavor in flavors])
except Exception:
msg = _("Unable to retrieve flavor list.")
exceptions.handle(self.request, msg)
def _task_get_instances():
try:
tmp_instances, self._more = api.nova.server_list(
self.request,
search_opts=search_opts,
all_tenants=True)
instances.extend(tmp_instances)
except Exception:
self._more = False
exceptions.handle(self.request,
_('Unable to retrieve instance list.'))
# In case of exception when calling nova.server_list
# don't call api.network
return
try:
api.network.servers_update_addresses(self.request, instances,
all_tenants=True)
except Exception:
exceptions.handle(
self.request,
message=_('Unable to retrieve IP addresses from Neutron.'),
ignore=True)
with futurist.ThreadPoolExecutor(max_workers=4) as e:
e.submit(fn=_task_get_tenants)
e.submit(fn=_task_get_images)
e.submit(fn=_task_get_flavors)
e.submit(fn=_task_get_instances)
# This code gets activated only in case of filtering by nonexistent
# project, image or flavor. Executing it before _task_get_instances
# would make Horizon make less API calls, but as a drawback would make
# it impossible to parallelize the request. Executing it after is
# a tradeoff, as it happens less often to filter by nonexistent values.
if 'project' in search_opts and \
not swap_filter(tenants, search_opts, 'project', 'tenant_id'):
@ -134,43 +185,22 @@ class AdminIndexView(tables.DataTableView):
self._more = False
return instances
try:
instances, self._more = api.nova.server_list(
self.request,
search_opts=search_opts,
all_tenants=True)
except Exception:
self._more = False
exceptions.handle(self.request,
_('Unable to retrieve instance list.'))
if instances:
# Loop through instances to get flavor and tenant info.
for inst in instances:
flavor_id = inst.flavor["id"]
try:
api.network.servers_update_addresses(self.request, instances,
all_tenants=True)
if flavor_id in full_flavors:
inst.full_flavor = full_flavors[flavor_id]
else:
# If the flavor_id is not in full_flavors list,
# gets it via nova api.
inst.full_flavor = api.nova.flavor_get(
self.request, flavor_id)
except Exception:
exceptions.handle(
self.request,
message=_('Unable to retrieve IP addresses from Neutron.'),
ignore=True)
full_flavors = OrderedDict([(f.id, f) for f in flavors])
tenant_dict = OrderedDict([(t.id, t) for t in tenants])
# Loop through instances to get flavor and tenant info.
for inst in instances:
flavor_id = inst.flavor["id"]
try:
if flavor_id in full_flavors:
inst.full_flavor = full_flavors[flavor_id]
else:
# If the flavor_id is not in full_flavors list,
# gets it via nova api.
inst.full_flavor = api.nova.flavor_get(
self.request, flavor_id)
except Exception:
msg = _('Unable to retrieve instance size information.')
exceptions.handle(self.request, msg)
tenant = tenant_dict.get(inst.tenant_id, None)
inst.tenant_name = getattr(tenant, "name", None)
msg = _('Unable to retrieve instance size information.')
exceptions.handle(self.request, msg)
tenant = tenant_dict.get(inst.tenant_id, None)
inst.tenant_name = getattr(tenant, "name", None)
return instances

View File

@ -20,6 +20,7 @@
Views for managing instances.
"""
from collections import OrderedDict
import futurist
import logging
from django.conf import settings
@ -63,47 +64,29 @@ class IndexView(tables.DataTableView):
return self._more
def get_data(self):
instances = []
marker = self.request.GET.get(
project_tables.InstancesTable._meta.pagination_param, None)
search_opts = self.get_filters({'marker': marker, 'paginate': True})
# Gather our flavors and images and correlate our instances to them
try:
flavors = api.nova.flavor_list(self.request)
except Exception:
flavors = []
exceptions.handle(self.request, ignore=True)
instances = []
full_flavors = {}
image_map = {}
try:
# TODO(gabriel): Handle pagination.
images = api.glance.image_list_detailed(self.request)[0]
except Exception:
images = []
exceptions.handle(self.request, ignore=True)
if 'image_name' in search_opts and \
not swap_filter(images, search_opts, 'image_name', 'image'):
def _task_get_instances():
# Gather our instances
try:
tmp_instances, self._more = api.nova.server_list(
self.request,
search_opts=search_opts)
instances.extend(tmp_instances)
except Exception:
self._more = False
return instances
elif 'flavor_name' in search_opts and \
not swap_filter(flavors, search_opts, 'flavor_name', 'flavor'):
self._more = False
return instances
exceptions.handle(self.request,
_('Unable to retrieve instances.'))
# In case of exception when calling nova.server_list
# don't call api.network
return
# Gather our instances
try:
instances, self._more = api.nova.server_list(
self.request,
search_opts=search_opts)
except Exception:
self._more = False
instances = []
exceptions.handle(self.request,
_('Unable to retrieve instances.'))
if instances:
try:
api.network.servers_update_addresses(self.request, instances)
except Exception:
@ -111,31 +94,57 @@ class IndexView(tables.DataTableView):
self.request,
message=_('Unable to retrieve IP addresses from Neutron.'),
ignore=True)
full_flavors = OrderedDict([(str(flavor.id), flavor)
for flavor in flavors])
image_map = OrderedDict([(str(image.id), image)
for image in images])
# Loop through instances to get flavor info.
for instance in instances:
if hasattr(instance, 'image'):
# Instance from image returns dict
if isinstance(instance.image, dict):
if instance.image.get('id') in image_map:
instance.image = image_map[instance.image['id']]
try:
flavor_id = instance.flavor["id"]
if flavor_id in full_flavors:
instance.full_flavor = full_flavors[flavor_id]
def _task_get_flavors():
# Gather our flavors to correlate our instances to them
try:
flavors = api.nova.flavor_list(self.request)
full_flavors.update([(str(flavor.id), flavor)
for flavor in flavors])
except Exception:
exceptions.handle(self.request, ignore=True)
def _task_get_images():
# Gather our images to correlate our instances to them
try:
# TODO(gabriel): Handle pagination.
images = api.glance.image_list_detailed(self.request)[0]
image_map.update([(str(image.id), image) for image in images])
except Exception:
exceptions.handle(self.request, ignore=True)
with futurist.ThreadPoolExecutor(max_workers=3) as e:
e.submit(fn=_task_get_instances)
e.submit(fn=_task_get_flavors)
e.submit(fn=_task_get_images)
# Loop through instances to get flavor info.
for instance in instances:
if hasattr(instance, 'image'):
# Instance from image returns dict
if isinstance(instance.image, dict):
if instance.image.get('id') in image_map:
instance.image = image_map[instance.image.get('id')]
# In case image not found in image_map, set name to empty
# to avoid fallback API call to Glance in api/nova.py
# until the call is deprecated in api itself
else:
# If the flavor_id is not in full_flavors list,
# get it via nova api.
instance.full_flavor = api.nova.flavor_get(
self.request, flavor_id)
except Exception:
msg = ('Unable to retrieve flavor "%s" for instance "%s".'
% (flavor_id, instance.id))
LOG.info(msg)
instance.image['name'] = _("-")
try:
flavor_id = instance.flavor["id"]
if flavor_id in full_flavors:
instance.full_flavor = full_flavors[flavor_id]
else:
# If the flavor_id is not in full_flavors list,
# get it via nova api.
instance.full_flavor = api.nova.flavor_get(
self.request, flavor_id)
except Exception:
msg = ('Unable to retrieve flavor "%s" for instance "%s".'
% (flavor_id, instance.id))
LOG.info(msg)
return instances