Merge ~twom/launchpad:stats-daemon into launchpad: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)
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
Colin Watson (cjwatson) wrote :

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.

review: Approve
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
1diff --git a/daemons/numbercruncher.tac b/daemons/numbercruncher.tac
2new file mode 100644
3index 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')
42diff --git a/lib/lp/buildmaster/manager.py b/lib/lp/buildmaster/manager.py
43index 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
139diff --git a/lib/lp/buildmaster/tests/test_manager.py b/lib/lp/buildmaster/tests/test_manager.py
140index 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- ]))
318diff --git a/lib/lp/services/statsd/numbercruncher.py b/lib/lp/services/statsd/numbercruncher.py
319new file mode 100644
320index 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
461diff --git a/lib/lp/services/statsd/tests/__init__.py b/lib/lp/services/statsd/tests/__init__.py
462index 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))
489diff --git a/lib/lp/services/statsd/tests/test_numbercruncher.py b/lib/lp/services/statsd/tests/test_numbercruncher.py
490new file mode 100644
491index 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+ ]))
610diff --git a/utilities/start-dev-soyuz.sh b/utilities/start-dev-soyuz.sh
611index 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

Subscribers

People subscribed via source and target branches

to status/vote changes: