Merge lp:~julian-edwards/launchpad/build-sched-bug-478691 into lp:launchpad

Proposed by Julian Edwards
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~julian-edwards/launchpad/build-sched-bug-478691
Merge into: lp:launchpad
Diff against target: 246 lines (+153/-50)
2 files modified
lib/lp/soyuz/model/builder.py (+33/-16)
lib/lp/soyuz/tests/test_builder.py (+120/-34)
To merge this branch: bzr merge lp:~julian-edwards/launchpad/build-sched-bug-478691
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) release-critical Approve
Brad Crittenden (community) code Approve
Review via email: mp+14699@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

= Summary =
Fix findBuildCandidate so that instead of only allowing one builder per arch
for each PPA, it allows up to 80% of the available builders for that arch.

== Proposed fix ==
As per https://bugs.edge.launchpad.net/soyuz/+bug/478691

To alleviate the problem of daily build PPAs from monopolising the build darm,
a change was made last cycle to allow PPAs' builds to only occupy at most one
builder of each architecture type. Unfortunately, this has proven to not only
prevent the daily builds from completing within 24 hours, it also leaves the
build farm under-utilised.

== Pre-implementation notes ==
I've spoken to some of the daily PPA owners (see the bug comments) and we've
decided to change the calculation slightly so that instead of only being able
to occupy one builder of each architecture at a time, it now only dispatches
the build if it would take the usage to 80% or more of available builders of
that arch.

== Implementation details ==
This was just a change to the big SQL query done in findBuildCandidate. I
also made the tests a little more generic in case we need to tweak this again
(we *will* need to tweak it again!).

== Tests ==
bin/test -cvv test_builder

== Demo and Q/A ==
Can't really Q/A this as we don't have multiple builders available in dogfood.
:(

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/soyuz/model/builder.py
  lib/lp/soyuz/tests/test_builder.py

Revision history for this message
Brad Crittenden (bac) wrote :

This looks good. Please make the change we discussed:

for build in builds_list[:num_builds]

review: Approve (code)
Revision history for this message
Francis J. Lacoste (flacoste) :
review: Approve (release-critical)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/soyuz/model/builder.py'
--- lib/lp/soyuz/model/builder.py 2009-10-14 09:43:42 +0000
+++ lib/lp/soyuz/model/builder.py 2009-11-11 10:46:12 +0000
@@ -569,22 +569,39 @@
569 archive.require_virtualized = %s569 archive.require_virtualized = %s
570 """ % sqlvalues(self.virtualized))570 """ % sqlvalues(self.virtualized))
571571
572 # Ensure that if a currently-building build exists for the same572 # Ensure that if BUILDING builds exist for the same
573 # public ppa archive and architecture currently building then573 # public ppa archive and architecture and another would not
574 # we don't consider another as a candidate.574 # leave at least 20% of them free, then we don't consider
575 clauses.append("""575 # another as a candidate.
576 NOT EXISTS (576 #
577 SELECT Build.id577 # This clause selects the count of currently building builds on
578 FROM Build build2, DistroArchSeries distroarchseries2578 # the arch in question, then adds one to that total before
579 WHERE579 # deriving a percentage of the total available builders on that
580 build2.archive = build.archive AND580 # arch. It then makes sure that percentage is under 80.
581 archive.purpose = %s AND581 #
582 archive.private IS FALSE AND582 # The extra clause is only used if the number of available
583 build2.distroarchseries = distroarchseries2.id AND583 # builders is greater than one, or nothing would get dispatched
584 distroarchseries2.processorfamily = %s AND584 # at all.
585 build2.buildstate = %s)585 num_arch_builders = Builder.selectBy(
586 """ % sqlvalues(586 processor=self.processor, manual=False, builderok=True).count()
587 ArchivePurpose.PPA, self.processor.family, BuildStatus.BUILDING))587 if num_arch_builders > 1:
588 clauses.append("""
589 EXISTS (SELECT true
590 WHERE ((
591 SELECT COUNT(build2.id)
592 FROM Build build2, DistroArchSeries distroarchseries2
593 WHERE
594 build2.archive = build.archive AND
595 archive.purpose = %s AND
596 archive.private IS FALSE AND
597 build2.distroarchseries = distroarchseries2.id AND
598 distroarchseries2.processorfamily = %s AND
599 build2.buildstate = %s) + 1::numeric)
600 *100 / %s
601 < 80)
602 """ % sqlvalues(
603 ArchivePurpose.PPA, self.processor.family,
604 BuildStatus.BUILDING, num_arch_builders))
588605
589 query = " AND ".join(clauses)606 query = " AND ".join(clauses)
590 candidate = BuildQueue.selectFirst(607 candidate = BuildQueue.selectFirst(
591608
=== modified file 'lib/lp/soyuz/tests/test_builder.py'
--- lib/lp/soyuz/tests/test_builder.py 2009-10-14 14:25:19 +0000
+++ lib/lp/soyuz/tests/test_builder.py 2009-11-11 10:46:12 +0000
@@ -26,14 +26,83 @@
26 self.publisher = SoyuzTestPublisher()26 self.publisher = SoyuzTestPublisher()
27 self.publisher.prepareBreezyAutotest()27 self.publisher.prepareBreezyAutotest()
2828
29 # Create two i386 builders ready to build PPA builds.29 # Create some i386 builders ready to build PPA builds. Two
30 builder_set = getUtility(IBuilderSet)30 # already exist in sampledata so we'll use those first.
31 self.builder1 = self.factory.makeBuilder(name='bob2')31 self.builder1 = getUtility(IBuilderSet)['bob']
32 self.builder2 = self.factory.makeBuilder(name='frog2')32 self.builder2 = getUtility(IBuilderSet)['frog']
33 self.builder3 = self.factory.makeBuilder(name='builder3')
34 self.builder4 = self.factory.makeBuilder(name='builder4')
35 self.builder5 = self.factory.makeBuilder(name='builder5')
36 self.builders = [
37 self.builder1,
38 self.builder2,
39 self.builder3,
40 self.builder4,
41 self.builder5,
42 ]
43
44 # Ensure all builders are operational.
45 for builder in self.builders:
46 builder.builderok = True
47 builder.manual = False
48
49
50class TestFindBuildCandidatePPAWithSingleBuilder(TestCaseWithFactory):
51
52 layer = LaunchpadZopelessLayer
53
54 def setUp(self):
55 super(TestFindBuildCandidatePPAWithSingleBuilder, self).setUp()
56 self.publisher = SoyuzTestPublisher()
57 self.publisher.prepareBreezyAutotest()
58
59 self.bob_builder = getUtility(IBuilderSet)['bob']
60 self.frog_builder = getUtility(IBuilderSet)['frog']
61
62 # Disable bob so only frog is available.
63 self.bob_builder.manual = True
64 self.bob_builder.builderok = True
65 self.frog_builder.manual = False
66 self.frog_builder.builderok = True
67
68 # Make a new PPA and give it some builds.
69 self.ppa_joe = self.factory.makeArchive(name="joesppa")
70 builds = self.publisher.getPubSource(
71 sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
72 archive=self.ppa_joe).createMissingBuilds()
73
74 def test_findBuildCandidate_first_build_started(self):
75 # The allocation rule for PPA dispatching doesn't apply when
76 # there's only one builder available.
77
78 # Asking frog to find a candidate should give us the joesppa build.
79 next_job = self.frog_builder.findBuildCandidate()
80 self.assertEqual('joesppa', next_job.build.archive.name)
81
82 # If bob is in a failed state the joesppa build is still
83 # returned.
84 self.bob_builder.builderok = False
85 self.bob_builder.manual = False
86 next_job = self.frog_builder.findBuildCandidate()
87 self.assertEqual('joesppa', next_job.build.archive.name)
3388
3489
35class TestFindBuildCandidatePPA(TestFindBuildCandidateBase):90class TestFindBuildCandidatePPA(TestFindBuildCandidateBase):
3691
92 def _setBuildsBuildingForArch(self, builds_list, num_builds,
93 archtag="i386"):
94 """Helper function.
95
96 Set the first `num_builds` in `builds_list` with `archtag` as
97 BUILDING.
98 """
99 count = 0
100 for build in builds_list[:num_builds]:
101 if build.distroarchseries.architecturetag == archtag:
102 build.buildstate = BuildStatus.BUILDING
103 build.builder = self.builders[count]
104 count += 1
105
37 def setUp(self):106 def setUp(self):
38 """Publish some builds for the test archive."""107 """Publish some builds for the test archive."""
39 super(TestFindBuildCandidatePPA, self).setUp()108 super(TestFindBuildCandidatePPA, self).setUp()
@@ -42,41 +111,58 @@
42 self.ppa_joe = self.factory.makeArchive(name="joesppa")111 self.ppa_joe = self.factory.makeArchive(name="joesppa")
43 self.ppa_jim = self.factory.makeArchive(name="jimsppa")112 self.ppa_jim = self.factory.makeArchive(name="jimsppa")
44113
45 self.publisher.getPubSource(114 self.joe_builds = []
46 sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,115 self.joe_builds.extend(
47 archive=self.ppa_joe).createMissingBuilds()116 self.publisher.getPubSource(
48 self.publisher.getPubSource(117 sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
49 sourcename="firefox", status=PackagePublishingStatus.PUBLISHED,118 archive=self.ppa_joe).createMissingBuilds())
50 archive=self.ppa_joe).createMissingBuilds()119 self.joe_builds.extend(
51120 self.publisher.getPubSource(
52 self.publisher.getPubSource(121 sourcename="firefox",
53 sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,122 status=PackagePublishingStatus.PUBLISHED,
54 archive=self.ppa_jim).createMissingBuilds()123 archive=self.ppa_joe).createMissingBuilds())
55 self.publisher.getPubSource(124 self.joe_builds.extend(
56 sourcename="firefox", status=PackagePublishingStatus.PUBLISHED,125 self.publisher.getPubSource(
57 archive=self.ppa_jim).createMissingBuilds()126 sourcename="cobblers",
58127 status=PackagePublishingStatus.PUBLISHED,
59 # Grab the first build, ensure that it is what we expect128 archive=self.ppa_joe).createMissingBuilds())
60 # (ie. the first build from joesppa) and set it building.129 self.joe_builds.extend(
61 self.first_job = self.builder1.findBuildCandidate()130 self.publisher.getPubSource(
62 self.failUnlessEqual('joesppa', self.first_job.build.archive.name)131 sourcename="thunderpants",
63 self.failUnlessEqual(132 status=PackagePublishingStatus.PUBLISHED,
64 u'i386 build of gedit 666 in ubuntutest breezy-autotest RELEASE',133 archive=self.ppa_joe).createMissingBuilds())
65 self.first_job.build.title)134
66 self.first_job.build.buildstate = BuildStatus.BUILDING135 self.jim_builds = []
67 self.first_job.build.builder = self.builder1136 self.jim_builds.extend(
137 self.publisher.getPubSource(
138 sourcename="gedit",
139 status=PackagePublishingStatus.PUBLISHED,
140 archive=self.ppa_jim).createMissingBuilds())
141 self.jim_builds.extend(
142 self.publisher.getPubSource(
143 sourcename="firefox",
144 status=PackagePublishingStatus.PUBLISHED,
145 archive=self.ppa_jim).createMissingBuilds())
146
147 # Set the first three builds in joe's PPA as building, which
148 # leaves two builders free.
149 self._setBuildsBuildingForArch(self.joe_builds, 3)
150 num_active_builders = len(
151 [build for build in self.joe_builds if build.builder is not None])
152 num_free_builders = len(self.builders) - num_active_builders
153 self.assertEqual(num_free_builders, 2)
68154
69 def test_findBuildCandidate_first_build_started(self):155 def test_findBuildCandidate_first_build_started(self):
70 # Once a build for an ppa+arch has started, a second one for the156 # A PPA cannot start a build if it would use 80% or more of the
71 # same ppa+arch will not be a candidate.157 # builders.
72 next_job = self.builder2.findBuildCandidate()158 next_job = self.builder4.findBuildCandidate()
73 self.failIfEqual('joesppa', next_job.build.archive.name)159 self.failIfEqual('joesppa', next_job.build.archive.name)
74160
75 def test_findBuildCandidate_first_build_finished(self):161 def test_findBuildCandidate_first_build_finished(self):
76 # When joe's first ppa build finishes, his second i386 build162 # When joe's first ppa build finishes, his fourth i386 build
77 # will be the next build candidate.163 # will be the next build candidate.
78 self.first_job.build.buildstate = BuildStatus.FAILEDTOBUILD164 self.joe_builds[0].buildstate = BuildStatus.FAILEDTOBUILD
79 next_job = self.builder2.findBuildCandidate()165 next_job = self.builder4.findBuildCandidate()
80 self.failUnlessEqual('joesppa', next_job.build.archive.name)166 self.failUnlessEqual('joesppa', next_job.build.archive.name)
81167
82 def test_findBuildCandidate_for_private_ppa(self):168 def test_findBuildCandidate_for_private_ppa(self):
@@ -84,7 +170,7 @@
84 # for the one architecture.170 # for the one architecture.
85 self.ppa_joe.private = True171 self.ppa_joe.private = True
86 self.ppa_joe.buildd_secret = 'sekrit'172 self.ppa_joe.buildd_secret = 'sekrit'
87 next_job = self.builder2.findBuildCandidate()173 next_job = self.builder4.findBuildCandidate()
88 self.failUnlessEqual('joesppa', next_job.build.archive.name)174 self.failUnlessEqual('joesppa', next_job.build.archive.name)
89175
90176