Merge ~ltrager/maas:2.3_ws_cache_current_scriptresults_only into maas:2.3

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: 740a4d8d5b0595e24c5c11e1e483b3a1831c74d8
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:2.3_ws_cache_current_scriptresults_only
Merge into: maas:2.3
Prerequisite: ~ltrager/maas:2.3_1722607_2
Diff against target: 358 lines (+95/-54)
6 files modified
src/maasserver/websockets/base.py (+8/-6)
src/maasserver/websockets/handlers/device.py (+9/-1)
src/maasserver/websockets/handlers/machine.py (+0/-11)
src/maasserver/websockets/handlers/node.py (+30/-25)
src/maasserver/websockets/handlers/tests/test_device.py (+1/-1)
src/maasserver/websockets/handlers/tests/test_machine.py (+47/-10)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+368112@code.launchpad.net

Commit message

Only cache processed ScriptResults for the current nodes being dehydrated.

Previously MAAS would cache the current ScriptResults for all nodes
to minimize database lookups. This caused all ScriptResults to be
loaded into RAM regardless of how many nodes are actually being
loaded. Now only the latest ScriptResults from the current set of
nodes being dehydrates are loaded. As nodes are dehydrated their
ScriptResults are cleared from cache to reduce memory usage on the
region.

Backport of 15a4c14 for LP: #1830365

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

LANDING
-b 2.3_ws_cache_current_scriptresults_only lp:~ltrager/maas/+git/maas into -b 2.3 lp:~maas-committers/maas

STATUS: FAILED BUILD
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/5797/consoleText

740a4d8... by Lee Trager

Merge branch '2.3' into 2.3_ws_cache_current_scriptresults_only

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/websockets/base.py b/src/maasserver/websockets/base.py
2index 11b7fe4..53fda92 100644
3--- a/src/maasserver/websockets/base.py
4+++ b/src/maasserver/websockets/base.py
5@@ -1,4 +1,4 @@
6-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
7+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
8 # GNU Affero General Public License version 3 (see the file LICENSE).
9
10 """The base class that all handlers must extend."""
11@@ -335,6 +335,11 @@ class Handler(metaclass=HandlerMetaclass):
12 else:
13 raise HandlerNoSuchMethodError(method_name)
14
15+ def _cache_pks(self, objs):
16+ """Cache all loaded object pks."""
17+ getpk = attrgetter(self._meta.pk)
18+ self.cache["loaded_pks"].update(getpk(obj) for obj in objs)
19+
20 def list(self, params):
21 """List objects.
22
23@@ -353,9 +358,7 @@ class Handler(metaclass=HandlerMetaclass):
24 if "limit" in params:
25 queryset = queryset[:params["limit"]]
26 objs = list(queryset)
27-
28- getpk = attrgetter(self._meta.pk)
29- self.cache["loaded_pks"].update(getpk(obj) for obj in objs)
30+ self._cache_pks(objs)
31 return [
32 self.full_dehydrate(obj, for_list=True)
33 for obj in objs
34@@ -367,8 +370,7 @@ class Handler(metaclass=HandlerMetaclass):
35 :param pk: Object with primary key to return.
36 """
37 obj = self.get_object(params)
38- getpk = attrgetter(self._meta.pk)
39- self.cache["loaded_pks"].add(getpk(obj))
40+ self._cache_pks([obj])
41 return self.full_dehydrate(obj)
42
43 def create(self, params):
44diff --git a/src/maasserver/websockets/handlers/device.py b/src/maasserver/websockets/handlers/device.py
45index c8af5de..7469821 100644
46--- a/src/maasserver/websockets/handlers/device.py
47+++ b/src/maasserver/websockets/handlers/device.py
48@@ -1,4 +1,4 @@
49-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
50+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
51 # GNU Affero General Public License version 3 (see the file LICENSE).
52
53 """The device handler for the WebSocket connection."""
54@@ -7,6 +7,8 @@ __all__ = [
55 "DeviceHandler",
56 ]
57
58+from operator import attrgetter
59+
60 from django.core.exceptions import ValidationError
61 from maasserver.enum import (
62 DEVICE_IP_ASSIGNMENT_TYPE,
63@@ -136,6 +138,12 @@ class DeviceHandler(NodeHandler):
64 "device",
65 ]
66
67+ def _cache_pks(self, objs):
68+ """Cache all loaded object pks."""
69+ # Copy from base.py as devices don't have ScriptResults
70+ getpk = attrgetter(self._meta.pk)
71+ self.cache["loaded_pks"].update(getpk(obj) for obj in objs)
72+
73 def get_queryset(self):
74 """Return `QuerySet` for devices only viewable by `user`."""
75 return Device.objects.get_nodes(
76diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
77index 4f16b5f..cd0e6db 100644
78--- a/src/maasserver/websockets/handlers/machine.py
79+++ b/src/maasserver/websockets/handlers/machine.py
80@@ -83,7 +83,6 @@ from metadataserver.enum import (
81 HARDWARE_TYPE,
82 RESULT_TYPE,
83 )
84-from metadataserver.models import ScriptResult
85 from metadataserver.models.scriptset import get_status_from_qs
86 from provisioningserver.logger import LegacyLogger
87 from provisioningserver.rpc.exceptions import UnknownPowerType
88@@ -192,16 +191,6 @@ class MachineHandler(NodeHandler):
89 self.default_osystem = Config.objects.get_config('default_osystem')
90 self.default_distro_series = Config.objects.get_config(
91 'default_distro_series')
92-
93- qs = ScriptResult.objects.all()
94- qs = qs.select_related('script_set', 'script')
95- qs = qs.order_by(
96- 'script_name', 'physical_blockdevice_id', 'script_set__node_id',
97- '-id')
98- qs = qs.distinct(
99- 'script_name', 'physical_blockdevice_id', 'script_set__node_id')
100- self._refresh_script_result_cache(qs)
101-
102 return super(MachineHandler, self).list(params)
103
104 def dehydrate(self, obj, data, for_list=False):
105diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
106index a2bc88d..d53e445 100644
107--- a/src/maasserver/websockets/handlers/node.py
108+++ b/src/maasserver/websockets/handlers/node.py
109@@ -1,4 +1,4 @@
110-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
111+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
112 # GNU Affero General Public License version 3 (see the file LICENSE).
113
114 """The node handler for the WebSocket connection."""
115@@ -51,6 +51,7 @@ from metadataserver.enum import (
116 SCRIPT_STATUS,
117 SCRIPT_STATUS_CHOICES,
118 )
119+from metadataserver.models.scriptresult import ScriptResult
120 from metadataserver.models.scriptset import get_status_from_qs
121 from provisioningserver.tags import merge_details_cleanly
122
123@@ -312,22 +313,26 @@ class NodeHandler(TimestampedModelHandler):
124
125 return data
126
127- def _refresh_script_result_cache(self, qs):
128- """Refresh the ScriptResult cache from the given qs.
129-
130- If a node_id is given only that node is refreshed.
131- """
132- cleared_node_ids = []
133- for script_result in qs:
134+ def _cache_pks(self, nodes):
135+ """Refresh the ScriptResult cache from the given node."""
136+ super()._cache_pks(nodes)
137+ script_results = ScriptResult.objects.filter(
138+ script_set__node__in=nodes)
139+ script_results = script_results.select_related('script_set', 'script')
140+ script_results = script_results.order_by(
141+ 'script_name', 'physical_blockdevice_id', 'script_set__node_id',
142+ '-id')
143+ script_results = script_results.distinct(
144+ 'script_name', 'physical_blockdevice_id', 'script_set__node_id')
145+ for script_result in script_results:
146 node_id = script_result.script_set.node_id
147 if script_result.script is not None:
148 hardware_type = script_result.script.hardware_type
149 else:
150 hardware_type = HARDWARE_TYPE.NODE
151
152- if node_id not in cleared_node_ids:
153+ if node_id not in self._script_results:
154 self._script_results[node_id] = {}
155- cleared_node_ids.append(node_id)
156
157 if hardware_type not in self._script_results[node_id]:
158 self._script_results[node_id][hardware_type] = []
159@@ -341,7 +346,7 @@ class NodeHandler(TimestampedModelHandler):
160 for i, cached_script_result in enumerate(
161 self._script_results[node_id][hardware_type]):
162 if cached_script_result.id == script_result.id:
163- self._script_results[node_id].pop(i)
164+ self._script_results[node_id][hardware_type].pop(i)
165 break
166 else:
167 self._script_results[node_id][hardware_type].append(
168@@ -629,8 +634,6 @@ class NodeHandler(TimestampedModelHandler):
169 def get_object(self, params):
170 """Get object by using the `pk` in `params`."""
171 obj = super(NodeHandler, self).get_object(params)
172- # Get the object and update update the script_result_cache.
173- self._refresh_script_result_cache(obj.get_latest_script_results)
174
175 if self.user.is_superuser:
176 return obj.as_self()
177@@ -706,12 +709,13 @@ class NodeHandler(TimestampedModelHandler):
178 node = self.get_object(params)
179 # Produce a "clean" composite details document.
180 details_template = dict.fromkeys(script_output_nsmap.values())
181- for hw_type in self._script_results.get(node.id, {}).values():
182- for script_result in hw_type:
183- if (script_result.name in script_output_nsmap and
184- script_result.status == SCRIPT_STATUS.PASSED):
185- namespace = script_output_nsmap[script_result.name]
186- details_template[namespace] = script_result.stdout
187+ for script_result in ScriptResult.objects.filter(
188+ script_name__in=script_output_nsmap.keys(),
189+ status=SCRIPT_STATUS.PASSED,
190+ script_set__node=node).order_by(
191+ 'script_name', '-updated').distinct('script_name'):
192+ namespace = script_output_nsmap[script_result.name]
193+ details_template[namespace] = script_result.stdout
194 probed_details = merge_details_cleanly(details_template)
195
196 # We check here if there's something to show instead of after
197@@ -728,12 +732,13 @@ class NodeHandler(TimestampedModelHandler):
198 node = self.get_object(params)
199 # Produce a "clean" composite details document.
200 details_template = dict.fromkeys(script_output_nsmap.values())
201- for hw_type in self._script_results.get(node.id, {}).values():
202- for script_result in hw_type:
203- if (script_result.name in script_output_nsmap and
204- script_result.status == SCRIPT_STATUS.PASSED):
205- namespace = script_output_nsmap[script_result.name]
206- details_template[namespace] = script_result.stdout
207+ for script_result in ScriptResult.objects.filter(
208+ script_name__in=script_output_nsmap.keys(),
209+ status=SCRIPT_STATUS.PASSED,
210+ script_set__node=node).order_by(
211+ 'script_name', '-updated').distinct('script_name'):
212+ namespace = script_output_nsmap[script_result.name]
213+ details_template[namespace] = script_result.stdout
214 probed_details = merge_details_cleanly(details_template)
215
216 # We check here if there's something to show instead of after
217diff --git a/src/maasserver/websockets/handlers/tests/test_device.py b/src/maasserver/websockets/handlers/tests/test_device.py
218index f7af49c..c26e303 100644
219--- a/src/maasserver/websockets/handlers/tests/test_device.py
220+++ b/src/maasserver/websockets/handlers/tests/test_device.py
221@@ -1,4 +1,4 @@
222-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
223+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
224 # GNU Affero General Public License version 3 (see the file LICENSE).
225
226 """Tests for `maasserver.websockets.handlers.device`"""
227diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
228index 299a5e8..c6bbbbf 100644
229--- a/src/maasserver/websockets/handlers/tests/test_machine.py
230+++ b/src/maasserver/websockets/handlers/tests/test_machine.py
231@@ -1,4 +1,4 @@
232-# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
233+# Copyright 2016-2018 Canonical Ltd. This software is licensed under the
234 # GNU Affero General Public License version 3 (see the file LICENSE).
235
236 """Tests for `maasserver.websockets.handlers.node`"""
237@@ -146,7 +146,7 @@ class TestMachineHandler(MAASServerTestCase):
238 def dehydrate_node(self, node, handler, for_list=False):
239 # Prime handler._script_results
240 handler._script_results = {}
241- handler._refresh_script_result_cache(node.get_latest_script_results)
242+ handler._cache_pks([node])
243
244 boot_interface = node.get_boot_interface()
245 pxe_mac_vendor = node.get_pxe_mac_vendor()
246@@ -407,7 +407,7 @@ class TestMachineHandler(MAASServerTestCase):
247 factory.make_Node(
248 node_type=NODE_TYPE.DEVICE, parent=node, interface=True)
249
250- def test_get_object_refresh_script_result_cache(self):
251+ def test_get_refresh_script_result_cache(self):
252 owner = factory.make_User()
253 node = factory.make_Node(owner=owner)
254 script_result = factory.make_ScriptResult(
255@@ -429,7 +429,7 @@ class TestMachineHandler(MAASServerTestCase):
256 }
257 handler = MachineHandler(owner, {})
258 handler._script_results[cached_node.id] = cached_content
259- handler.get_object({'system_id': node.system_id})
260+ handler._cache_pks([node])
261
262 self.assertEquals(
263 script_result.id,
264@@ -444,7 +444,7 @@ class TestMachineHandler(MAASServerTestCase):
265 self.assertEquals(
266 cached_content, handler._script_results[cached_node.id])
267
268- def test_get_object_refresh_script_result_cache_clears_aborted(self):
269+ def test_get_refresh_script_result_cache_clears_aborted(self):
270 # Regression test for LP:1731350
271 owner = factory.make_User()
272 node = factory.make_Node(owner=owner)
273@@ -459,11 +459,45 @@ class TestMachineHandler(MAASServerTestCase):
274 # Simulate aborting commissioning/testing
275 script_result.status = SCRIPT_STATUS.ABORTED
276 script_result.save()
277- handler.get_object({'system_id': node.system_id})
278+ handler._cache_pks([node])
279
280 self.assertItemsEqual([], handler._script_results[node.id][
281 script_result.script.hardware_type])
282
283+ def test_list_num_queries_is_the_expected_number(self):
284+ owner = factory.make_User()
285+ for _ in range(10):
286+ node = factory.make_Node(owner=owner)
287+ commissioning_script_set = factory.make_ScriptSet(
288+ node=node, result_type=RESULT_TYPE.COMMISSIONING)
289+ testing_script_set = factory.make_ScriptSet(
290+ node=node, result_type=RESULT_TYPE.TESTING)
291+ node.current_commissioning_script_set = commissioning_script_set
292+ node.current_testing_script_set = testing_script_set
293+ node.save()
294+ for __ in range(10):
295+ factory.make_ScriptResult(
296+ status=SCRIPT_STATUS.PASSED,
297+ script_set=commissioning_script_set)
298+ factory.make_ScriptResult(
299+ status=SCRIPT_STATUS.PASSED,
300+ script_set=testing_script_set)
301+
302+ handler = MachineHandler(owner, {})
303+ queries_one, _ = count_queries(handler.list, {'limit': 1})
304+ queries_total, _ = count_queries(handler.list, {})
305+ # This check is to notify the developer that a change was made that
306+ # affects the number of queries performed when doing a node listing.
307+ # It is important to keep this number as low as possible. A larger
308+ # number means regiond has to do more work slowing down its process
309+ # and slowing down the client waiting for the response.
310+ self.assertEqual(
311+ queries_one, 12,
312+ "Number of queries has changed; make sure this is expected.")
313+ self.assertEqual(
314+ queries_total, 12,
315+ "Number of queries has changed; make sure this is expected.")
316+
317 def test_dehydrate_owner_empty_when_None(self):
318 owner = factory.make_User()
319 handler = MachineHandler(owner, {})
320@@ -1502,7 +1536,7 @@ class TestMachineHandler(MAASServerTestCase):
321 user = factory.make_User()
322 handler = MachineHandler(user, {})
323 node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=user)
324- factory.make_ScriptResult(
325+ script_result = factory.make_ScriptResult(
326 script_set=factory.make_ScriptSet(node=node),
327 status=SCRIPT_STATUS.PASSED)
328 factory.make_PhysicalBlockDevice(node)
329@@ -1510,7 +1544,10 @@ class TestMachineHandler(MAASServerTestCase):
330 self.assertItemsEqual(
331 [self.dehydrate_node(node, handler, for_list=True)],
332 handler.list({}))
333- self.assertIn(node.id, handler._script_results.keys())
334+ self.assertDictEqual(
335+ {node.id: {
336+ script_result.script.hardware_type: [script_result]}},
337+ handler._script_results)
338
339 def test_list_ignores_devices(self):
340 owner = factory.make_User()
341@@ -2673,7 +2710,7 @@ class TestMachineHandler(MAASServerTestCase):
342 new_name = factory.make_name("name")
343 new_vlan = factory.make_VLAN()
344 handler._script_results = {}
345- handler._refresh_script_result_cache(node.get_latest_script_results)
346+ handler._cache_pks([node])
347 handler.update_interface({
348 "system_id": node.system_id,
349 "interface_id": interface.id,
350@@ -2691,7 +2728,7 @@ class TestMachineHandler(MAASServerTestCase):
351 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
352 new_name = factory.make_name("name")
353 handler._script_results = {}
354- handler._refresh_script_result_cache(node.get_latest_script_results)
355+ handler._cache_pks([node])
356 handler.update_interface({
357 "system_id": node.system_id,
358 "interface_id": interface.id,

Subscribers

People subscribed via source and target branches