Merge ~twom/launchpad:stats-daemon into launchpad:master
- Git
- lp:~twom/launchpad
- stats-daemon
- Merge into master
Proposed by
Tom Wardill
Status: | Merged |
---|---|
Approved by: | Tom Wardill |
Approved revision: | 6b2665a9168b38b7ebc1a72563c64278372a2437 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~twom/launchpad:stats-daemon |
Merge into: | launchpad:master |
Prerequisite: | ~twom/launchpad:stats-actual-build-queues |
Diff against target: |
621 lines (+314/-179) 7 files modified
daemons/numbercruncher.tac (+35/-0) lib/lp/buildmaster/manager.py (+1/-54) lib/lp/buildmaster/tests/test_manager.py (+2/-125) lib/lp/services/statsd/numbercruncher.py (+137/-0) lib/lp/services/statsd/tests/__init__.py (+23/-0) lib/lp/services/statsd/tests/test_numbercruncher.py (+115/-0) utilities/start-dev-soyuz.sh (+1/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+389910@code.launchpad.net |
Commit message
Split timed stats into separate daemon
Description of the change
Ideally we want generation/updating of stats to not block actual work. Move the timed stats generation out of buildd-manager to it's own daemon.
Leave the event driven stats in buildd-manager.
The implementation of the daemon here was heavily based on (and imports part of) buildd-manager itself, as that seemed the simplest method to allow reuse of the existing vitals generation code.
To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) : | # |
~twom/launchpad:stats-daemon
updated
- 6b2665a... by Tom Wardill
-
Fix typo
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/daemons/numbercruncher.tac b/daemons/numbercruncher.tac |
2 | new file mode 100644 |
3 | index 0000000..e9a7bf7 |
4 | --- /dev/null |
5 | +++ b/daemons/numbercruncher.tac |
6 | @@ -0,0 +1,35 @@ |
7 | +# Copyright 2009-202 Canonical Ltd. This software is licensed under the |
8 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
9 | + |
10 | +# Twisted Application Configuration file. |
11 | +# Use with "twistd2.4 -y <file.tac>", e.g. "twistd -noy server.tac" |
12 | + |
13 | + |
14 | +from twisted.application import service |
15 | +from twisted.scripts.twistd import ServerOptions |
16 | + |
17 | +from lp.services.daemons import readyservice |
18 | +from lp.services.scripts import execute_zcml_for_scripts |
19 | +from lp.services.statsd.numbercruncher import NumberCruncher |
20 | +from lp.services.twistedsupport.features import setup_feature_controller |
21 | +from lp.services.twistedsupport.loggingsupport import RotatableFileLogObserver |
22 | + |
23 | +execute_zcml_for_scripts() |
24 | + |
25 | +options = ServerOptions() |
26 | +options.parseOptions() |
27 | + |
28 | +application = service.Application('BuilddManager') |
29 | +application.addComponent( |
30 | + RotatableFileLogObserver(options.get('logfile')), ignoreClass=1) |
31 | + |
32 | +# Service that announces when the daemon is ready. |
33 | +readyservice.ReadyService().setServiceParent(application) |
34 | + |
35 | + |
36 | +# Service for updating statsd receivers. |
37 | +service = NumberCruncher() |
38 | +service.setServiceParent(application) |
39 | + |
40 | +# Allow use of feature flags. |
41 | +setup_feature_controller('number-cruncher') |
42 | diff --git a/lib/lp/buildmaster/manager.py b/lib/lp/buildmaster/manager.py |
43 | index d0aa32e..7bbe4aa 100644 |
44 | --- a/lib/lp/buildmaster/manager.py |
45 | +++ b/lib/lp/buildmaster/manager.py |
46 | @@ -8,6 +8,7 @@ __metaclass__ = type |
47 | __all__ = [ |
48 | 'BuilddManager', |
49 | 'BUILDD_MANAGER_LOG_NAME', |
50 | + 'PrefetchedBuilderFactory', |
51 | 'SlaveScanner', |
52 | ] |
53 | |
54 | @@ -705,9 +706,6 @@ class BuilddManager(service.Service): |
55 | # How often to flush logtail updates, in seconds. |
56 | FLUSH_LOGTAILS_INTERVAL = 15 |
57 | |
58 | - # How often to update stats, in seconds |
59 | - UPDATE_STATS_INTERVAL = 60 |
60 | - |
61 | def __init__(self, clock=None, builder_factory=None): |
62 | # Use the clock if provided, it's so that tests can |
63 | # advance it. Use the reactor by default. |
64 | @@ -739,52 +737,6 @@ class BuilddManager(service.Service): |
65 | logger.setLevel(level) |
66 | return logger |
67 | |
68 | - def _updateBuilderCounts(self): |
69 | - """Update statsd with the builder statuses.""" |
70 | - self.logger.debug("Updating builder stats.") |
71 | - counts_by_processor = {} |
72 | - for builder in self.builder_factory.iterVitals(): |
73 | - if not builder.active: |
74 | - continue |
75 | - for processor_name in builder.processor_names: |
76 | - counts = counts_by_processor.setdefault( |
77 | - "{},virtualized={}".format( |
78 | - processor_name, |
79 | - builder.virtualized), |
80 | - {'cleaning': 0, 'idle': 0, 'disabled': 0, 'building': 0}) |
81 | - if not builder.builderok: |
82 | - counts['disabled'] += 1 |
83 | - elif builder.clean_status == BuilderCleanStatus.CLEANING: |
84 | - counts['cleaning'] += 1 |
85 | - elif (builder.build_queue and |
86 | - builder.build_queue.status == BuildQueueStatus.RUNNING): |
87 | - counts['building'] += 1 |
88 | - elif builder.clean_status == BuilderCleanStatus.CLEAN: |
89 | - counts['idle'] += 1 |
90 | - for processor, counts in counts_by_processor.items(): |
91 | - for count_name, count_value in counts.items(): |
92 | - gauge_name = "builders.{},arch={}".format( |
93 | - count_name, processor) |
94 | - self.logger.debug("{}: {}".format(gauge_name, count_value)) |
95 | - self.statsd_client.gauge(gauge_name, count_value) |
96 | - self.logger.debug("Builder stats update complete.") |
97 | - |
98 | - def _updateBuilderQueues(self): |
99 | - """Update statsd with the build queue lengths.""" |
100 | - self.logger.debug("Updating build queue stats.") |
101 | - queue_details = getUtility(IBuilderSet).getBuildQueueSizes() |
102 | - for queue_type, contents in queue_details.items(): |
103 | - virt = queue_type == 'virt' |
104 | - for arch, value in contents.items(): |
105 | - gauge_name = "buildqueue,virtualized={},arch={}".format( |
106 | - virt, arch) |
107 | - self.statsd_client.gauge(gauge_name, value[0]) |
108 | - self.logger.debug("Build queue stats update complete.") |
109 | - |
110 | - def updateStats(self): |
111 | - self._updateBuilderCounts() |
112 | - self._updateBuilderQueues() |
113 | - |
114 | def checkForNewBuilders(self): |
115 | """Add and return any new builders.""" |
116 | new_builders = set( |
117 | @@ -854,9 +806,6 @@ class BuilddManager(service.Service): |
118 | # Schedule bulk flushes for build queue logtail updates. |
119 | self.flush_logtails_loop, self.flush_logtails_deferred = ( |
120 | self._startLoop(self.FLUSH_LOGTAILS_INTERVAL, self.flushLogTails)) |
121 | - # Schedule stats updates. |
122 | - self.stats_update_loop, self.stats_update_deferred = ( |
123 | - self._startLoop(self.UPDATE_STATS_INTERVAL, self.updateStats)) |
124 | |
125 | def stopService(self): |
126 | """Callback for when we need to shut down.""" |
127 | @@ -865,11 +814,9 @@ class BuilddManager(service.Service): |
128 | deferreds = [slave.stopping_deferred for slave in self.builder_slaves] |
129 | deferreds.append(self.scan_builders_deferred) |
130 | deferreds.append(self.flush_logtails_deferred) |
131 | - deferreds.append(self.stats_update_deferred) |
132 | |
133 | self.flush_logtails_loop.stop() |
134 | self.scan_builders_loop.stop() |
135 | - self.stats_update_loop.stop() |
136 | for slave in self.builder_slaves: |
137 | slave.stopCycle() |
138 | |
139 | diff --git a/lib/lp/buildmaster/tests/test_manager.py b/lib/lp/buildmaster/tests/test_manager.py |
140 | index c7c8028..d9bbf9e 100644 |
141 | --- a/lib/lp/buildmaster/tests/test_manager.py |
142 | +++ b/lib/lp/buildmaster/tests/test_manager.py |
143 | @@ -14,10 +14,7 @@ import signal |
144 | import time |
145 | |
146 | from six.moves import xmlrpc_client |
147 | -from testtools.matchers import ( |
148 | - Equals, |
149 | - MatchesListwise, |
150 | - ) |
151 | +from testtools.matchers import Equals |
152 | from testtools.testcase import ExpectedException |
153 | from testtools.twistedsupport import AsynchronousDeferredRunTest |
154 | import transaction |
155 | @@ -48,7 +45,6 @@ from lp.buildmaster.interfaces.builder import ( |
156 | IBuilderSet, |
157 | ) |
158 | from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet |
159 | -from lp.buildmaster.interfaces.processor import IProcessorSet |
160 | from lp.buildmaster.manager import ( |
161 | BuilddManager, |
162 | BUILDER_FAILURE_THRESHOLD, |
163 | @@ -75,10 +71,9 @@ from lp.buildmaster.tests.test_interactor import ( |
164 | MockBuilderFactory, |
165 | ) |
166 | from lp.registry.interfaces.distribution import IDistributionSet |
167 | -from lp.services.compat import mock |
168 | from lp.services.config import config |
169 | from lp.services.log.logger import BufferLogger |
170 | -from lp.services.statsd.interfaces.statsd_client import IStatsdClient |
171 | +from lp.services.statsd.tests import StatsMixin |
172 | from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet |
173 | from lp.soyuz.model.binarypackagebuildbehaviour import ( |
174 | BinaryPackageBuildBehaviour, |
175 | @@ -93,7 +88,6 @@ from lp.testing import ( |
176 | from lp.testing.dbuser import switch_dbuser |
177 | from lp.testing.factory import LaunchpadObjectFactory |
178 | from lp.testing.fakemethod import FakeMethod |
179 | -from lp.testing.fixture import ZopeUtilityFixture |
180 | from lp.testing.layers import ( |
181 | LaunchpadScriptLayer, |
182 | LaunchpadZopelessLayer, |
183 | @@ -103,16 +97,6 @@ from lp.testing.matchers import HasQueryCount |
184 | from lp.testing.sampledata import BOB_THE_BUILDER_NAME |
185 | |
186 | |
187 | -class StatsMixin: |
188 | - |
189 | - def setUpStats(self): |
190 | - # Install a mock statsd client so we can assert against the call |
191 | - # counts and args. |
192 | - self.stats_client = mock.Mock() |
193 | - self.useFixture( |
194 | - ZopeUtilityFixture(self.stats_client, IStatsdClient)) |
195 | - |
196 | - |
197 | class TestSlaveScannerScan(StatsMixin, TestCaseWithFactory): |
198 | """Tests `SlaveScanner.scan` method. |
199 | |
200 | @@ -1240,22 +1224,6 @@ class TestBuilddManager(TestCase): |
201 | clock.advance(advance) |
202 | self.assertNotEqual(0, manager.flushLogTails.call_count) |
203 | |
204 | - def test_startService_adds_updateStats_loop(self): |
205 | - # When startService is called, the manager will start up a |
206 | - # updateStats loop. |
207 | - self._stub_out_scheduleNextScanCycle() |
208 | - clock = task.Clock() |
209 | - manager = BuilddManager(clock=clock) |
210 | - |
211 | - # Replace updateStats() with FakeMethod so we can see if it was |
212 | - # called. |
213 | - manager.updateStats = FakeMethod() |
214 | - |
215 | - manager.startService() |
216 | - advance = BuilddManager.UPDATE_STATS_INTERVAL + 1 |
217 | - clock.advance(advance) |
218 | - self.assertNotEqual(0, manager.updateStats.call_count) |
219 | - |
220 | |
221 | class TestFailureAssessments(TestCaseWithFactory): |
222 | |
223 | @@ -1645,94 +1613,3 @@ class TestBuilddManagerScript(TestCaseWithFactory): |
224 | self.assertFalse( |
225 | os.access(rotated_logfilepath, os.F_OK), |
226 | "Twistd's log file was rotated by twistd.") |
227 | - |
228 | - |
229 | -class TestStats(StatsMixin, TestCaseWithFactory): |
230 | - |
231 | - layer = ZopelessDatabaseLayer |
232 | - run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=20) |
233 | - |
234 | - def setUp(self): |
235 | - super(TestStats, self).setUp() |
236 | - self.setUpStats() |
237 | - |
238 | - def test_single_processor_counts(self): |
239 | - builder = self.factory.makeBuilder() |
240 | - builder.setCleanStatus(BuilderCleanStatus.CLEAN) |
241 | - self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave())) |
242 | - transaction.commit() |
243 | - clock = task.Clock() |
244 | - manager = BuilddManager(clock=clock) |
245 | - manager._updateBuilderQueues = FakeMethod() |
246 | - manager.builder_factory.update() |
247 | - manager.updateStats() |
248 | - |
249 | - self.assertEqual(8, self.stats_client.gauge.call_count) |
250 | - for call in self.stats_client.mock.gauge.call_args_list: |
251 | - self.assertIn('386', call[0][0]) |
252 | - |
253 | - def test_multiple_processor_counts(self): |
254 | - builder = self.factory.makeBuilder( |
255 | - processors=[getUtility(IProcessorSet).getByName('amd64')]) |
256 | - builder.setCleanStatus(BuilderCleanStatus.CLEAN) |
257 | - self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave())) |
258 | - transaction.commit() |
259 | - clock = task.Clock() |
260 | - manager = BuilddManager(clock=clock) |
261 | - manager._updateBuilderQueues = FakeMethod() |
262 | - manager.builder_factory.update() |
263 | - manager.updateStats() |
264 | - |
265 | - self.assertEqual(12, self.stats_client.gauge.call_count) |
266 | - i386_calls = [c for c in self.stats_client.gauge.call_args_list |
267 | - if '386' in c[0][0]] |
268 | - amd64_calls = [c for c in self.stats_client.gauge.call_args_list |
269 | - if 'amd64' in c[0][0]] |
270 | - self.assertEqual(8, len(i386_calls)) |
271 | - self.assertEqual(4, len(amd64_calls)) |
272 | - |
273 | - def test_correct_values_counts(self): |
274 | - builder = self.factory.makeBuilder( |
275 | - processors=[getUtility(IProcessorSet).getByName('amd64')]) |
276 | - builder.setCleanStatus(BuilderCleanStatus.CLEANING) |
277 | - self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave())) |
278 | - transaction.commit() |
279 | - clock = task.Clock() |
280 | - manager = BuilddManager(clock=clock) |
281 | - manager._updateBuilderQueues = FakeMethod() |
282 | - manager.builder_factory.update() |
283 | - manager.updateStats() |
284 | - |
285 | - self.assertEqual(12, self.stats_client.gauge.call_count) |
286 | - calls = [c[0] for c in self.stats_client.gauge.call_args_list |
287 | - if 'amd64' in c[0][0]] |
288 | - self.assertThat( |
289 | - calls, MatchesListwise( |
290 | - [Equals(('builders.disabled,arch=amd64,virtualized=True', 0)), |
291 | - Equals(('builders.building,arch=amd64,virtualized=True', 0)), |
292 | - Equals(('builders.idle,arch=amd64,virtualized=True', 0)), |
293 | - Equals(('builders.cleaning,arch=amd64,virtualized=True', 1)) |
294 | - ])) |
295 | - |
296 | - def test_updateBuilderQueues(self): |
297 | - builder = self.factory.makeBuilder( |
298 | - processors=[getUtility(IProcessorSet).getByName('amd64')]) |
299 | - builder.setCleanStatus(BuilderCleanStatus.CLEANING) |
300 | - build = self.factory.makeSnapBuild() |
301 | - build.queueBuild() |
302 | - self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave())) |
303 | - transaction.commit() |
304 | - clock = task.Clock() |
305 | - manager = BuilddManager(clock=clock) |
306 | - manager._updateBuilderCounts = FakeMethod() |
307 | - manager.builder_factory.update() |
308 | - manager.updateStats() |
309 | - |
310 | - self.assertEqual(2, self.stats_client.gauge.call_count) |
311 | - self.assertThat( |
312 | - [x[0] for x in self.stats_client.gauge.call_args_list], |
313 | - MatchesListwise( |
314 | - [Equals(('buildqueue,virtualized=True,arch={}'.format( |
315 | - build.processor.name), 1)), |
316 | - Equals(('buildqueue,virtualized=False,arch=386', 1)) |
317 | - ])) |
318 | diff --git a/lib/lp/services/statsd/numbercruncher.py b/lib/lp/services/statsd/numbercruncher.py |
319 | new file mode 100644 |
320 | index 0000000..0798719 |
321 | --- /dev/null |
322 | +++ b/lib/lp/services/statsd/numbercruncher.py |
323 | @@ -0,0 +1,137 @@ |
324 | +# Copyright 2020 Canonical Ltd. This software is licensed under the |
325 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
326 | + |
327 | +"""Out of process statsd reporting.""" |
328 | + |
329 | +from __future__ import absolute_import, print_function, unicode_literals |
330 | + |
331 | +__metaclass__ = type |
332 | +__all__ = ['NumberCruncher'] |
333 | + |
334 | +import logging |
335 | + |
336 | +from twisted.application import service |
337 | +from twisted.internet import ( |
338 | + defer, |
339 | + reactor, |
340 | + ) |
341 | +from twisted.internet.task import LoopingCall |
342 | +from twisted.python import log |
343 | +from zope.component import getUtility |
344 | + |
345 | +from lp.buildmaster.enums import ( |
346 | + BuilderCleanStatus, |
347 | + BuildQueueStatus, |
348 | + ) |
349 | +from lp.buildmaster.interfaces.builder import IBuilderSet |
350 | +from lp.buildmaster.manager import PrefetchedBuilderFactory |
351 | +from lp.services.statsd.interfaces.statsd_client import IStatsdClient |
352 | + |
353 | +NUMBER_CRUNCHER_LOG_NAME = "number-cruncher" |
354 | + |
355 | + |
356 | +class NumberCruncher(service.Service): |
357 | + """Export statistics to statsd.""" |
358 | + |
359 | + QUEUE_INTERVAL = 60 |
360 | + BUILDER_INTERVAL = 60 |
361 | + |
362 | + def __init__(self, clock=None, builder_factory=None): |
363 | + if clock is None: |
364 | + clock = reactor |
365 | + self._clock = clock |
366 | + self.logger = self._setupLogger() |
367 | + self.builder_factory = builder_factory or PrefetchedBuilderFactory() |
368 | + self.statsd_client = getUtility(IStatsdClient) |
369 | + |
370 | + def _setupLogger(self): |
371 | + """Set up a 'number-cruncher' logger that redirects to twisted. |
372 | + """ |
373 | + level = logging.INFO |
374 | + logger = logging.getLogger(NUMBER_CRUNCHER_LOG_NAME) |
375 | + logger.propagate = False |
376 | + |
377 | + # Redirect the output to the twisted log module. |
378 | + channel = logging.StreamHandler(log.StdioOnnaStick()) |
379 | + channel.setLevel(level) |
380 | + channel.setFormatter(logging.Formatter('%(message)s')) |
381 | + |
382 | + logger.addHandler(channel) |
383 | + logger.setLevel(level) |
384 | + return logger |
385 | + |
386 | + def _startLoop(self, interval, callback): |
387 | + """Schedule `callback` to run every `interval` seconds.""" |
388 | + loop = LoopingCall(callback) |
389 | + loop.clock = self._clock |
390 | + stopping_deferred = loop.start(interval) |
391 | + return loop, stopping_deferred |
392 | + |
393 | + def updateBuilderQueues(self): |
394 | + """Update statsd with the build queue lengths.""" |
395 | + self.logger.debug("Updating build queue stats.") |
396 | + queue_details = getUtility(IBuilderSet).getBuildQueueSizes() |
397 | + for queue_type, contents in queue_details.items(): |
398 | + virt = queue_type == 'virt' |
399 | + for arch, value in contents.items(): |
400 | + gauge_name = "buildqueue,virtualized={},arch={}".format( |
401 | + virt, arch) |
402 | + self.logger.debug("{}: {}".format(gauge_name, value[0])) |
403 | + self.statsd_client.gauge(gauge_name, value[0]) |
404 | + self.logger.debug("Build queue stats update complete.") |
405 | + |
406 | + def _updateBuilderCounts(self): |
407 | + """Update statsd with the builder statuses. |
408 | + |
409 | + Requires the builder_factory to be updated. |
410 | + """ |
411 | + self.logger.debug("Updating builder stats.") |
412 | + counts_by_processor = {} |
413 | + for builder in self.builder_factory.iterVitals(): |
414 | + if not builder.active: |
415 | + continue |
416 | + for processor_name in builder.processor_names: |
417 | + counts = counts_by_processor.setdefault( |
418 | + "{},virtualized={}".format( |
419 | + processor_name, |
420 | + builder.virtualized), |
421 | + {'cleaning': 0, 'idle': 0, 'disabled': 0, 'building': 0}) |
422 | + if not builder.builderok: |
423 | + counts['disabled'] += 1 |
424 | + elif builder.clean_status == BuilderCleanStatus.CLEANING: |
425 | + counts['cleaning'] += 1 |
426 | + elif (builder.build_queue and |
427 | + builder.build_queue.status == BuildQueueStatus.RUNNING): |
428 | + counts['building'] += 1 |
429 | + elif builder.clean_status == BuilderCleanStatus.CLEAN: |
430 | + counts['idle'] += 1 |
431 | + for processor, counts in counts_by_processor.items(): |
432 | + for count_name, count_value in counts.items(): |
433 | + gauge_name = "builders.{},arch={}".format( |
434 | + count_name, processor) |
435 | + self.logger.debug("{}: {}".format(gauge_name, count_value)) |
436 | + self.statsd_client.gauge(gauge_name, count_value) |
437 | + self.logger.debug("Builder stats update complete.") |
438 | + |
439 | + def updateBuilderStats(self): |
440 | + """Statistics that require builder knowledge to be updated.""" |
441 | + self.builder_factory.update() |
442 | + self._updateBuilderCounts() |
443 | + |
444 | + def startService(self): |
445 | + self.logger.INFO("Starting number-cruncher service.") |
446 | + self.update_queues_loop, self.update_queues_deferred = ( |
447 | + self._startLoop(self.QUEUE_INTERVAL, self.updateBuilderQueues)) |
448 | + self.update_builder_loop, self.update_builder_deferred = ( |
449 | + self._startLoop(self.BUILDER_INTERVAL, self.updateBuilderStats)) |
450 | + |
451 | + def stopService(self): |
452 | + deferreds = [] |
453 | + deferreds.append(self.update_queues_deferred) |
454 | + deferreds.append(self.update_builder_deferred) |
455 | + |
456 | + self.update_queues_loop.stop() |
457 | + self.update_builder_loop.stop() |
458 | + |
459 | + d = defer.DeferredList(deferreds, consumeErrors=True) |
460 | + return d |
461 | diff --git a/lib/lp/services/statsd/tests/__init__.py b/lib/lp/services/statsd/tests/__init__.py |
462 | index e69de29..20885a4 100644 |
463 | --- a/lib/lp/services/statsd/tests/__init__.py |
464 | +++ b/lib/lp/services/statsd/tests/__init__.py |
465 | @@ -0,0 +1,23 @@ |
466 | +# Copyright 2020 Canonical Ltd. This software is licensed under the |
467 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
468 | + |
469 | +"""Utility mixins for testing statsd handling""" |
470 | + |
471 | +from __future__ import absolute_import, print_function, unicode_literals |
472 | + |
473 | +__metaclass__ = type |
474 | +__all__ = ['StatsMixin'] |
475 | + |
476 | +from lp.services.compat import mock |
477 | +from lp.services.statsd.interfaces.statsd_client import IStatsdClient |
478 | +from lp.testing.fixture import ZopeUtilityFixture |
479 | + |
480 | + |
481 | +class StatsMixin: |
482 | + |
483 | + def setUpStats(self): |
484 | + # Install a mock statsd client so we can assert against the call |
485 | + # counts and args. |
486 | + self.stats_client = mock.Mock() |
487 | + self.useFixture( |
488 | + ZopeUtilityFixture(self.stats_client, IStatsdClient)) |
489 | diff --git a/lib/lp/services/statsd/tests/test_numbercruncher.py b/lib/lp/services/statsd/tests/test_numbercruncher.py |
490 | new file mode 100644 |
491 | index 0000000..d812037 |
492 | --- /dev/null |
493 | +++ b/lib/lp/services/statsd/tests/test_numbercruncher.py |
494 | @@ -0,0 +1,115 @@ |
495 | +# Copyright 2020 Canonical Ltd. This software is licensed under the |
496 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
497 | + |
498 | +"""Tests for the stats number cruncher daemon.""" |
499 | + |
500 | +from __future__ import absolute_import, print_function, unicode_literals |
501 | + |
502 | +__metaclass__ = type |
503 | + |
504 | +from testtools.matchers import ( |
505 | + Equals, |
506 | + MatchesListwise, |
507 | + ) |
508 | +from testtools.twistedsupport import AsynchronousDeferredRunTest |
509 | +import transaction |
510 | +from twisted.internet import task |
511 | +from zope.component import getUtility |
512 | + |
513 | +from lp.buildmaster.enums import BuilderCleanStatus |
514 | +from lp.buildmaster.interactor import BuilderSlave |
515 | +from lp.buildmaster.interfaces.processor import IProcessorSet |
516 | +from lp.buildmaster.tests.mock_slaves import OkSlave |
517 | +from lp.services.statsd.numbercruncher import NumberCruncher |
518 | +from lp.services.statsd.tests import StatsMixin |
519 | +from lp.testing import TestCaseWithFactory |
520 | +from lp.testing.fakemethod import FakeMethod |
521 | +from lp.testing.layers import ZopelessDatabaseLayer |
522 | + |
523 | + |
524 | +class TestNumberCruncher(StatsMixin, TestCaseWithFactory): |
525 | + |
526 | + layer = ZopelessDatabaseLayer |
527 | + run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=20) |
528 | + |
529 | + def setUp(self): |
530 | + super(TestNumberCruncher, self).setUp() |
531 | + self.setUpStats() |
532 | + |
533 | + def test_single_processor_counts(self): |
534 | + builder = self.factory.makeBuilder() |
535 | + builder.setCleanStatus(BuilderCleanStatus.CLEAN) |
536 | + self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave())) |
537 | + transaction.commit() |
538 | + clock = task.Clock() |
539 | + manager = NumberCruncher(clock=clock) |
540 | + manager.builder_factory.update() |
541 | + manager.updateBuilderStats() |
542 | + |
543 | + self.assertEqual(8, self.stats_client.gauge.call_count) |
544 | + for call in self.stats_client.mock.gauge.call_args_list: |
545 | + self.assertIn('386', call[0][0]) |
546 | + |
547 | + def test_multiple_processor_counts(self): |
548 | + builder = self.factory.makeBuilder( |
549 | + processors=[getUtility(IProcessorSet).getByName('amd64')]) |
550 | + builder.setCleanStatus(BuilderCleanStatus.CLEAN) |
551 | + self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave())) |
552 | + transaction.commit() |
553 | + clock = task.Clock() |
554 | + manager = NumberCruncher(clock=clock) |
555 | + manager.builder_factory.update() |
556 | + manager.updateBuilderStats() |
557 | + |
558 | + self.assertEqual(12, self.stats_client.gauge.call_count) |
559 | + i386_calls = [c for c in self.stats_client.gauge.call_args_list |
560 | + if '386' in c[0][0]] |
561 | + amd64_calls = [c for c in self.stats_client.gauge.call_args_list |
562 | + if 'amd64' in c[0][0]] |
563 | + self.assertEqual(8, len(i386_calls)) |
564 | + self.assertEqual(4, len(amd64_calls)) |
565 | + |
566 | + def test_correct_values_counts(self): |
567 | + builder = self.factory.makeBuilder( |
568 | + processors=[getUtility(IProcessorSet).getByName('amd64')]) |
569 | + builder.setCleanStatus(BuilderCleanStatus.CLEANING) |
570 | + self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave())) |
571 | + transaction.commit() |
572 | + clock = task.Clock() |
573 | + manager = NumberCruncher(clock=clock) |
574 | + manager.builder_factory.update() |
575 | + manager.updateBuilderStats() |
576 | + |
577 | + self.assertEqual(12, self.stats_client.gauge.call_count) |
578 | + calls = [c[0] for c in self.stats_client.gauge.call_args_list |
579 | + if 'amd64' in c[0][0]] |
580 | + self.assertThat( |
581 | + calls, MatchesListwise( |
582 | + [Equals(('builders.disabled,arch=amd64,virtualized=True', 0)), |
583 | + Equals(('builders.building,arch=amd64,virtualized=True', 0)), |
584 | + Equals(('builders.idle,arch=amd64,virtualized=True', 0)), |
585 | + Equals(('builders.cleaning,arch=amd64,virtualized=True', 1)) |
586 | + ])) |
587 | + |
588 | + def test_updateBuilderQueues(self): |
589 | + builder = self.factory.makeBuilder( |
590 | + processors=[getUtility(IProcessorSet).getByName('amd64')]) |
591 | + builder.setCleanStatus(BuilderCleanStatus.CLEANING) |
592 | + build = self.factory.makeSnapBuild() |
593 | + build.queueBuild() |
594 | + self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave())) |
595 | + transaction.commit() |
596 | + clock = task.Clock() |
597 | + manager = NumberCruncher(clock=clock) |
598 | + manager._updateBuilderCounts = FakeMethod() |
599 | + manager.builder_factory.update() |
600 | + manager.updateBuilderQueues() |
601 | + |
602 | + self.assertEqual(2, self.stats_client.gauge.call_count) |
603 | + self.assertThat( |
604 | + [x[0] for x in self.stats_client.gauge.call_args_list], |
605 | + MatchesListwise( |
606 | + [Equals(('buildqueue,virtualized=True,arch={}'.format( |
607 | + build.processor.name), 1)), |
608 | + Equals(('buildqueue,virtualized=False,arch=386', 1)) |
609 | + ])) |
610 | diff --git a/utilities/start-dev-soyuz.sh b/utilities/start-dev-soyuz.sh |
611 | index c151c1c..efe4b0f 100755 |
612 | --- a/utilities/start-dev-soyuz.sh |
613 | +++ b/utilities/start-dev-soyuz.sh |
614 | @@ -28,6 +28,7 @@ start_twistd_plugin() { |
615 | |
616 | start_twistd testkeyserver lib/lp/testing/keyserver/testkeyserver.tac |
617 | start_twistd buildd-manager daemons/buildd-manager.tac |
618 | +start_twistd numbercruncher daemons/numbercruncher.tac |
619 | mkdir -p /var/tmp/txpkgupload/incoming |
620 | start_twistd_plugin txpkgupload pkgupload \ |
621 | --config-file configs/development/txpkgupload.yaml |
Overall this looks pretty good. It feels like the sort of thing that's likely to want to be restructured a bit once it covers more of the application, but that's fine.