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
1=== modified file 'lib/lp/soyuz/model/builder.py'
2--- lib/lp/soyuz/model/builder.py 2009-10-14 09:43:42 +0000
3+++ lib/lp/soyuz/model/builder.py 2009-11-11 10:46:12 +0000
4@@ -569,22 +569,39 @@
5 archive.require_virtualized = %s
6 """ % sqlvalues(self.virtualized))
7
8- # Ensure that if a currently-building build exists for the same
9- # public ppa archive and architecture currently building then
10- # we don't consider another as a candidate.
11- clauses.append("""
12- NOT EXISTS (
13- SELECT Build.id
14- FROM Build build2, DistroArchSeries distroarchseries2
15- WHERE
16- build2.archive = build.archive AND
17- archive.purpose = %s AND
18- archive.private IS FALSE AND
19- build2.distroarchseries = distroarchseries2.id AND
20- distroarchseries2.processorfamily = %s AND
21- build2.buildstate = %s)
22- """ % sqlvalues(
23- ArchivePurpose.PPA, self.processor.family, BuildStatus.BUILDING))
24+ # Ensure that if BUILDING builds exist for the same
25+ # public ppa archive and architecture and another would not
26+ # leave at least 20% of them free, then we don't consider
27+ # another as a candidate.
28+ #
29+ # This clause selects the count of currently building builds on
30+ # the arch in question, then adds one to that total before
31+ # deriving a percentage of the total available builders on that
32+ # arch. It then makes sure that percentage is under 80.
33+ #
34+ # The extra clause is only used if the number of available
35+ # builders is greater than one, or nothing would get dispatched
36+ # at all.
37+ num_arch_builders = Builder.selectBy(
38+ processor=self.processor, manual=False, builderok=True).count()
39+ if num_arch_builders > 1:
40+ clauses.append("""
41+ EXISTS (SELECT true
42+ WHERE ((
43+ SELECT COUNT(build2.id)
44+ FROM Build build2, DistroArchSeries distroarchseries2
45+ WHERE
46+ build2.archive = build.archive AND
47+ archive.purpose = %s AND
48+ archive.private IS FALSE AND
49+ build2.distroarchseries = distroarchseries2.id AND
50+ distroarchseries2.processorfamily = %s AND
51+ build2.buildstate = %s) + 1::numeric)
52+ *100 / %s
53+ < 80)
54+ """ % sqlvalues(
55+ ArchivePurpose.PPA, self.processor.family,
56+ BuildStatus.BUILDING, num_arch_builders))
57
58 query = " AND ".join(clauses)
59 candidate = BuildQueue.selectFirst(
60
61=== modified file 'lib/lp/soyuz/tests/test_builder.py'
62--- lib/lp/soyuz/tests/test_builder.py 2009-10-14 14:25:19 +0000
63+++ lib/lp/soyuz/tests/test_builder.py 2009-11-11 10:46:12 +0000
64@@ -26,14 +26,83 @@
65 self.publisher = SoyuzTestPublisher()
66 self.publisher.prepareBreezyAutotest()
67
68- # Create two i386 builders ready to build PPA builds.
69- builder_set = getUtility(IBuilderSet)
70- self.builder1 = self.factory.makeBuilder(name='bob2')
71- self.builder2 = self.factory.makeBuilder(name='frog2')
72+ # Create some i386 builders ready to build PPA builds. Two
73+ # already exist in sampledata so we'll use those first.
74+ self.builder1 = getUtility(IBuilderSet)['bob']
75+ self.builder2 = getUtility(IBuilderSet)['frog']
76+ self.builder3 = self.factory.makeBuilder(name='builder3')
77+ self.builder4 = self.factory.makeBuilder(name='builder4')
78+ self.builder5 = self.factory.makeBuilder(name='builder5')
79+ self.builders = [
80+ self.builder1,
81+ self.builder2,
82+ self.builder3,
83+ self.builder4,
84+ self.builder5,
85+ ]
86+
87+ # Ensure all builders are operational.
88+ for builder in self.builders:
89+ builder.builderok = True
90+ builder.manual = False
91+
92+
93+class TestFindBuildCandidatePPAWithSingleBuilder(TestCaseWithFactory):
94+
95+ layer = LaunchpadZopelessLayer
96+
97+ def setUp(self):
98+ super(TestFindBuildCandidatePPAWithSingleBuilder, self).setUp()
99+ self.publisher = SoyuzTestPublisher()
100+ self.publisher.prepareBreezyAutotest()
101+
102+ self.bob_builder = getUtility(IBuilderSet)['bob']
103+ self.frog_builder = getUtility(IBuilderSet)['frog']
104+
105+ # Disable bob so only frog is available.
106+ self.bob_builder.manual = True
107+ self.bob_builder.builderok = True
108+ self.frog_builder.manual = False
109+ self.frog_builder.builderok = True
110+
111+ # Make a new PPA and give it some builds.
112+ self.ppa_joe = self.factory.makeArchive(name="joesppa")
113+ builds = self.publisher.getPubSource(
114+ sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
115+ archive=self.ppa_joe).createMissingBuilds()
116+
117+ def test_findBuildCandidate_first_build_started(self):
118+ # The allocation rule for PPA dispatching doesn't apply when
119+ # there's only one builder available.
120+
121+ # Asking frog to find a candidate should give us the joesppa build.
122+ next_job = self.frog_builder.findBuildCandidate()
123+ self.assertEqual('joesppa', next_job.build.archive.name)
124+
125+ # If bob is in a failed state the joesppa build is still
126+ # returned.
127+ self.bob_builder.builderok = False
128+ self.bob_builder.manual = False
129+ next_job = self.frog_builder.findBuildCandidate()
130+ self.assertEqual('joesppa', next_job.build.archive.name)
131
132
133 class TestFindBuildCandidatePPA(TestFindBuildCandidateBase):
134
135+ def _setBuildsBuildingForArch(self, builds_list, num_builds,
136+ archtag="i386"):
137+ """Helper function.
138+
139+ Set the first `num_builds` in `builds_list` with `archtag` as
140+ BUILDING.
141+ """
142+ count = 0
143+ for build in builds_list[:num_builds]:
144+ if build.distroarchseries.architecturetag == archtag:
145+ build.buildstate = BuildStatus.BUILDING
146+ build.builder = self.builders[count]
147+ count += 1
148+
149 def setUp(self):
150 """Publish some builds for the test archive."""
151 super(TestFindBuildCandidatePPA, self).setUp()
152@@ -42,41 +111,58 @@
153 self.ppa_joe = self.factory.makeArchive(name="joesppa")
154 self.ppa_jim = self.factory.makeArchive(name="jimsppa")
155
156- self.publisher.getPubSource(
157- sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
158- archive=self.ppa_joe).createMissingBuilds()
159- self.publisher.getPubSource(
160- sourcename="firefox", status=PackagePublishingStatus.PUBLISHED,
161- archive=self.ppa_joe).createMissingBuilds()
162-
163- self.publisher.getPubSource(
164- sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
165- archive=self.ppa_jim).createMissingBuilds()
166- self.publisher.getPubSource(
167- sourcename="firefox", status=PackagePublishingStatus.PUBLISHED,
168- archive=self.ppa_jim).createMissingBuilds()
169-
170- # Grab the first build, ensure that it is what we expect
171- # (ie. the first build from joesppa) and set it building.
172- self.first_job = self.builder1.findBuildCandidate()
173- self.failUnlessEqual('joesppa', self.first_job.build.archive.name)
174- self.failUnlessEqual(
175- u'i386 build of gedit 666 in ubuntutest breezy-autotest RELEASE',
176- self.first_job.build.title)
177- self.first_job.build.buildstate = BuildStatus.BUILDING
178- self.first_job.build.builder = self.builder1
179+ self.joe_builds = []
180+ self.joe_builds.extend(
181+ self.publisher.getPubSource(
182+ sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
183+ archive=self.ppa_joe).createMissingBuilds())
184+ self.joe_builds.extend(
185+ self.publisher.getPubSource(
186+ sourcename="firefox",
187+ status=PackagePublishingStatus.PUBLISHED,
188+ archive=self.ppa_joe).createMissingBuilds())
189+ self.joe_builds.extend(
190+ self.publisher.getPubSource(
191+ sourcename="cobblers",
192+ status=PackagePublishingStatus.PUBLISHED,
193+ archive=self.ppa_joe).createMissingBuilds())
194+ self.joe_builds.extend(
195+ self.publisher.getPubSource(
196+ sourcename="thunderpants",
197+ status=PackagePublishingStatus.PUBLISHED,
198+ archive=self.ppa_joe).createMissingBuilds())
199+
200+ self.jim_builds = []
201+ self.jim_builds.extend(
202+ self.publisher.getPubSource(
203+ sourcename="gedit",
204+ status=PackagePublishingStatus.PUBLISHED,
205+ archive=self.ppa_jim).createMissingBuilds())
206+ self.jim_builds.extend(
207+ self.publisher.getPubSource(
208+ sourcename="firefox",
209+ status=PackagePublishingStatus.PUBLISHED,
210+ archive=self.ppa_jim).createMissingBuilds())
211+
212+ # Set the first three builds in joe's PPA as building, which
213+ # leaves two builders free.
214+ self._setBuildsBuildingForArch(self.joe_builds, 3)
215+ num_active_builders = len(
216+ [build for build in self.joe_builds if build.builder is not None])
217+ num_free_builders = len(self.builders) - num_active_builders
218+ self.assertEqual(num_free_builders, 2)
219
220 def test_findBuildCandidate_first_build_started(self):
221- # Once a build for an ppa+arch has started, a second one for the
222- # same ppa+arch will not be a candidate.
223- next_job = self.builder2.findBuildCandidate()
224+ # A PPA cannot start a build if it would use 80% or more of the
225+ # builders.
226+ next_job = self.builder4.findBuildCandidate()
227 self.failIfEqual('joesppa', next_job.build.archive.name)
228
229 def test_findBuildCandidate_first_build_finished(self):
230- # When joe's first ppa build finishes, his second i386 build
231+ # When joe's first ppa build finishes, his fourth i386 build
232 # will be the next build candidate.
233- self.first_job.build.buildstate = BuildStatus.FAILEDTOBUILD
234- next_job = self.builder2.findBuildCandidate()
235+ self.joe_builds[0].buildstate = BuildStatus.FAILEDTOBUILD
236+ next_job = self.builder4.findBuildCandidate()
237 self.failUnlessEqual('joesppa', next_job.build.archive.name)
238
239 def test_findBuildCandidate_for_private_ppa(self):
240@@ -84,7 +170,7 @@
241 # for the one architecture.
242 self.ppa_joe.private = True
243 self.ppa_joe.buildd_secret = 'sekrit'
244- next_job = self.builder2.findBuildCandidate()
245+ next_job = self.builder4.findBuildCandidate()
246 self.failUnlessEqual('joesppa', next_job.build.archive.name)
247
248