Lazy load the entire stack instead of single outputs

Each call to output_show is taking around 15 seconds for me, which
causes the entire inventory process to take 90 seconds or more.
Loading the entire stack with outputs resolved only takes around
20 seconds and keeps the total runtime of the dynamic inventory
much shorter overall because it only has to be done once.

The stack is lazy loaded at the first call to __getitem__ because it
seems that Ansible instantiates multiple copies of the class but
never actually looks anything up in some of them, so if we load the
stack in the constructor we just waste time retrieving it
unnecessarily.

This change has reduced the time it takes to run even simple ad-hoc
commands against a small 2 node inventory from 2.5-3 minutes to
45 seconds (which is still too long IMHO, but a step in the right
direction).

Closes-Bug: #1715428
Change-Id: I0fcde3f7641ccc26f045fcc8146ec4067e45330c
(cherry picked from commit 0dec2d8afa)
This commit is contained in:
Ben Nemec 2017-08-29 16:56:43 +00:00 committed by Steven Hardy
parent 67faa393d8
commit efe8a7251f
2 changed files with 33 additions and 27 deletions

View File

@ -23,32 +23,43 @@ HOST_NETWORK = 'ctlplane'
class StackOutputs(object):
"""Item getter for stack outputs.
Some stack outputs take a while to return via the API. This class
makes sure all outputs of a stack are fully recognized, while only
calling `stack.output_get` for the ones we're really using.
It takes a long time to resolve stack outputs. This class ensures that
we only have to do it once and then reuse the results from that call in
subsequent lookups. It also lazy loads the outputs so we don't spend time
on unnecessary Heat calls.
"""
def __init__(self, plan, hclient):
self.plan = plan
self.outputs = {}
self.hclient = hclient
try:
self.output_list = [
output['output_key'] for output in
self.hclient.stacks.output_list(plan)['outputs']]
except HTTPNotFound:
self.output_list = []
self.stack = None
def _load_outputs(self):
"""Load outputs from the stack if necessary
Retrieves the stack outputs if that has not already happened. If it
has then this is a noop.
Sets the outputs to an empty dict if the stack is not found.
"""
if not self.stack:
try:
self.stack = self.hclient.stacks.get(self.plan)
except HTTPNotFound:
self.outputs = {}
return
self.outputs = {i['output_key']: i['output_value']
for i in self.stack.outputs
}
def __getitem__(self, key):
if key not in self.output_list:
raise KeyError(key)
if key not in self.outputs:
self.outputs[key] = self.hclient.stacks.output_show(
self.plan, key)['output']['output_value']
self._load_outputs()
return self.outputs[key]
def __iter__(self):
return iter(self.output_list)
self._load_outputs()
return iter(self.outputs.keys())
def get(self, key, default=None):
try:

View File

@ -88,17 +88,12 @@ class TestInventory(base.TestCase):
'ctlplane': ['z.z.z.1']}}}]}
self.plan_name = 'overcloud'
def _mock_out_show(plan_name, key):
self.assertEqual(self.plan_name, plan_name)
out_data = [o for o in self.outputs_data['outputs']
if o['output_key'] == key][0]
return {'output': out_data}
self.hclient = MagicMock()
self.hclient.stacks.output_list.return_value = self.outputs_data
self.hclient.stacks.output_show.side_effect = _mock_out_show
self.hclient.stacks.environment.return_value = {
'parameter_defaults': {'AdminPassword': 'theadminpw'}}
self.mock_stack = MagicMock()
self.mock_stack.outputs = self.outputs_data['outputs']
self.hclient.stacks.get.return_value = self.mock_stack
self.configs = MagicMock()
self.configs.plan = self.plan_name
@ -133,7 +128,7 @@ class TestInventory(base.TestCase):
self.assertDictEqual(services, expected)
def test_outputs_are_empty_if_stack_doesnt_exist(self):
self.hclient.stacks.output_list.side_effect = HTTPNotFound('not found')
self.hclient.stacks.get.side_effect = HTTPNotFound('not found')
stack_outputs = StackOutputs('no-plan', self.hclient)
self.assertEqual(list(stack_outputs), [])
@ -156,9 +151,9 @@ class TestInventory(base.TestCase):
def test_outputs_iterating_returns_list_of_output_keys(self):
self.assertEqual(
['EnabledServices', 'KeystoneURL', 'ServerIdData',
'RoleNetHostnameMap', 'RoleNetIpMap'],
[o for o in self.outputs])
{'EnabledServices', 'KeystoneURL', 'ServerIdData',
'RoleNetHostnameMap', 'RoleNetIpMap'},
set([o for o in self.outputs]))
def test_inventory_list(self):
expected = {'c-0': {'hosts': ['x.x.x.1'],