Merge lp:~wgrant/launchpad/buildd-manager-handoff into lp:launchpad

Proposed by William Grant on 2015-02-19
Status: Merged
Merged at revision: 17349
Proposed branch: lp:~wgrant/launchpad/buildd-manager-handoff
Merge into: lp:launchpad
Prerequisite: lp:~wgrant/launchpad/buildd-manager-robustification
Diff against target: 226 lines (+64/-28)
5 files modified
lib/lp/archiveuploader/tests/test_uploadprocessor.py (+30/-7)
lib/lp/archiveuploader/uploadprocessor.py (+14/-3)
lib/lp/buildmaster/model/buildfarmjobbehaviour.py (+17/-12)
lib/lp/buildmaster/tests/test_manager.py (+1/-3)
lib/lp/translations/model/translationtemplatesbuildbehaviour.py (+2/-3)
To merge this branch: bzr merge lp:~wgrant/launchpad/buildd-manager-handoff
Reviewer Review Type Date Requested Status
Colin Watson 2015-02-19 Approve on 2015-02-19
Review via email: mp+250246@code.launchpad.net

Commit Message

Hand builds off from buildd-manager to process-upload atomically. UPLOADING is only set as the BuildQueue is destroyed, and binary uploads for invalid statuses are failed rather than ignored.

Description of the Change

Hand builds off from buildd-manager to process-upload atomically. buildd-manager now only sets UPLOADING in the same transaction as the BuildQueue destruction, ensuring that process-upload doesn't touch the build until buildd-manager is guaranteed to never touch it again. process-upload also now fails any upload that has a build which is neither BUILDING nor UPLOADING, as that would indicate a bug.

To post a comment you must log in.
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
2--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2015-01-23 17:57:59 +0000
3+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2015-02-19 12:31:10 +0000
4@@ -142,6 +142,7 @@
5
6 self.queue_folder = tempfile.mkdtemp()
7 self.incoming_folder = os.path.join(self.queue_folder, "incoming")
8+ self.failed_folder = os.path.join(self.queue_folder, "failed")
9 os.makedirs(self.incoming_folder)
10
11 self.test_files_dir = os.path.join(config.root,
12@@ -2258,9 +2259,7 @@
13 self.assertIsNot(None, build.duration)
14 self.assertIs(None, build.upload_log)
15
16- def testBuildWithInvalidStatus(self):
17- # Builds with an invalid (non-UPLOADING) status should trigger
18- # a warning but be left alone.
19+ def processUploadWithBuildStatus(self, status):
20 upload_dir = self.queueUpload("bar_1.0-1")
21 self.processUpload(self.uploadprocessor, upload_dir)
22 source_pub = self.publishPackage('bar', '1.0-1')
23@@ -2272,9 +2271,10 @@
24 status=PackageUploadStatus.ACCEPTED,
25 version=u"1.0-1", name=u"bar")
26 queue_item.setDone()
27+ stub.test_emails.pop()
28
29 build.buildqueue_record.markAsBuilding(self.factory.makeBuilder())
30- build.updateStatus(BuildStatus.BUILDING)
31+ build.updateStatus(status)
32 self.switchToUploader()
33
34 shutil.rmtree(upload_dir)
35@@ -2287,17 +2287,40 @@
36 "bar_1.0-1_binary", queue_entry=leaf_name)
37 self.options.context = 'buildd'
38 self.options.builds = True
39- len(stub.test_emails)
40+ self.assertEqual([], stub.test_emails)
41 BuildUploadHandler(self.uploadprocessor, self.incoming_folder,
42 leaf_name).process()
43 self.layer.txn.commit()
44+
45+ return build, leaf_name
46+
47+ def testBuildStillBuilding(self):
48+ # Builds that are still BUILDING should be left alone. The
49+ # upload directory may already be in place, but buildd-manager
50+ # will set the status to UPLOADING when it's handed off.
51+ build, leaf_name = self.processUploadWithBuildStatus(
52+ BuildStatus.BUILDING)
53 # The build status is not changed
54 self.assertTrue(
55 os.path.exists(os.path.join(self.incoming_folder, leaf_name)))
56 self.assertEquals(BuildStatus.BUILDING, build.status)
57+ self.assertLogContains("Build status is BUILDING. Ignoring.")
58+
59+ def testBuildWithInvalidStatus(self):
60+ # Builds with an invalid (not UPLOADING or BUILDING) status
61+ # should trigger a failure. We've probably raced with
62+ # buildd-manager due to a new and assuredly extra-special bug.
63+ build, leaf_name = self.processUploadWithBuildStatus(
64+ BuildStatus.NEEDSBUILD)
65+ # The build status is not changed, but the upload has moved.
66+ self.assertFalse(
67+ os.path.exists(os.path.join(self.incoming_folder, leaf_name)))
68+ self.assertTrue(
69+ os.path.exists(os.path.join(self.failed_folder, leaf_name)))
70+ self.assertEquals(BuildStatus.NEEDSBUILD, build.status)
71 self.assertLogContains(
72- "Expected build status to be 'UPLOADING', was BUILDING. "
73- "Ignoring.")
74+ "Expected build status to be UPLOADING or BUILDING, was "
75+ "NEEDSBUILD.")
76
77 def testOrderFilenames(self):
78 """orderFilenames sorts _source.changes ahead of other files."""
79
80=== modified file 'lib/lp/archiveuploader/uploadprocessor.py'
81--- lib/lp/archiveuploader/uploadprocessor.py 2015-01-23 17:57:59 +0000
82+++ lib/lp/archiveuploader/uploadprocessor.py 2015-02-19 12:31:10 +0000
83@@ -618,10 +618,21 @@
84 Build uploads always contain a single package per leaf.
85 """
86 logger = BufferLogger()
87- if self.build.status != BuildStatus.UPLOADING:
88+ if self.build.status == BuildStatus.BUILDING:
89+ # Handoff from buildd-manager isn't complete until the
90+ # status is UPLOADING.
91+ self.processor.log.info("Build status is BUILDING. Ignoring.")
92+ return
93+ elif self.build.status != BuildStatus.UPLOADING:
94+ # buildd-manager should only have given us a directory
95+ # during BUILDING or UPLOADING. Anything else indicates a
96+ # bug, so get the upload out of the queue before the status
97+ # changes to something that might accidentally let it be
98+ # accepted.
99 self.processor.log.warn(
100- "Expected build status to be 'UPLOADING', was %s. Ignoring." %
101+ "Expected build status to be UPLOADING or BUILDING, was %s.",
102 self.build.status.name)
103+ self.moveUpload(UploadStatusEnum.FAILED, logger)
104 return
105 try:
106 # The recipe may have been deleted so we need to flag that here
107@@ -659,7 +670,7 @@
108 if recipe_deleted:
109 # For a deleted recipe, no need to notify that uploading has
110 # failed - we just log a warning.
111- self.processor.log.warn(
112+ self.processor.log.info(
113 "Recipe for build %s was deleted. Ignoring." %
114 self.upload)
115 else:
116
117=== modified file 'lib/lp/buildmaster/model/buildfarmjobbehaviour.py'
118--- lib/lp/buildmaster/model/buildfarmjobbehaviour.py 2015-02-15 23:33:23 +0000
119+++ lib/lp/buildmaster/model/buildfarmjobbehaviour.py 2015-02-19 12:31:10 +0000
120@@ -220,6 +220,7 @@
121 'Processing finished job %s (%s) from builder %s: %s'
122 % (self.build.build_cookie, self.build.title,
123 self.build.buildqueue_record.builder.name, status))
124+ build_status = None
125 if status == 'OK':
126 yield self.storeLogFromSlave()
127 # handleSuccess will sometimes perform write operations
128@@ -227,18 +228,26 @@
129 # here and the commit can cause duplicated results. For
130 # example, a BinaryPackageBuild will end up in the upload
131 # queue twice if notify() crashes.
132- yield self.handleSuccess(slave_status, logger)
133+ build_status = yield self.handleSuccess(slave_status, logger)
134 elif status in fail_status_map:
135 # XXX wgrant: The builder should be set long before here, but
136 # currently isn't.
137 yield self.storeLogFromSlave()
138- self.build.updateStatus(
139- fail_status_map[status],
140- builder=self.build.buildqueue_record.builder,
141- slave_status=slave_status)
142+ build_status = fail_status_map[status]
143 else:
144 raise BuildDaemonError(
145 "Build returned unexpected status: %r" % status)
146+
147+ # Set the status and dequeue the build atomically. Setting the
148+ # status to UPLOADING constitutes handoff to process-upload, so
149+ # doing that before we've removed the BuildQueue causes races.
150+
151+ # XXX wgrant: The builder should be set long before here, but
152+ # currently isn't.
153+ self.build.updateStatus(
154+ build_status,
155+ builder=self.build.buildqueue_record.builder,
156+ slave_status=slave_status)
157 if notify:
158 self.build.notify()
159 self.build.buildqueue_record.destroySelf()
160@@ -260,8 +269,7 @@
161 if build.job_type == BuildFarmJobType.PACKAGEBUILD:
162 build = build.buildqueue_record.specific_build
163 if not build.current_source_publication:
164- build.updateStatus(BuildStatus.SUPERSEDED)
165- return
166+ defer.returnValue(BuildStatus.SUPERSEDED)
167
168 self.verifySuccessfulBuild()
169
170@@ -295,11 +303,6 @@
171 filenames_to_download.append((filemap[filename], out_file_name))
172 yield self._slave.getFiles(filenames_to_download)
173
174- # XXX wgrant: The builder should be set long before here, but
175- # currently isn't.
176- build.updateStatus(
177- BuildStatus.UPLOADING, builder=build.buildqueue_record.builder,
178- slave_status=slave_status)
179 transaction.commit()
180
181 # Move the directory used to grab the binaries into incoming
182@@ -312,3 +315,5 @@
183 if not os.path.exists(target_dir):
184 os.mkdir(target_dir)
185 os.rename(grab_dir, os.path.join(target_dir, upload_leaf))
186+
187+ defer.returnValue(BuildStatus.UPLOADING)
188
189=== modified file 'lib/lp/buildmaster/tests/test_manager.py'
190--- lib/lp/buildmaster/tests/test_manager.py 2015-02-17 07:57:31 +0000
191+++ lib/lp/buildmaster/tests/test_manager.py 2015-02-19 12:31:10 +0000
192@@ -582,9 +582,7 @@
193 # Mock out the build behaviour's handleSuccess so it doesn't
194 # try to upload things to the librarian or queue.
195 def handleSuccess(self, slave_status, logger):
196- build.updateStatus(
197- BuildStatus.UPLOADING, builder, slave_status=slave_status)
198- transaction.commit()
199+ return BuildStatus.UPLOADING
200 self.patch(
201 BinaryPackageBuildBehaviour, 'handleSuccess', handleSuccess)
202
203
204=== modified file 'lib/lp/translations/model/translationtemplatesbuildbehaviour.py'
205--- lib/lp/translations/model/translationtemplatesbuildbehaviour.py 2014-06-26 12:33:51 +0000
206+++ lib/lp/translations/model/translationtemplatesbuildbehaviour.py 2015-02-19 12:31:10 +0000
207@@ -116,7 +116,6 @@
208 # dangerous.
209 if filename is None:
210 logger.error("Build produced no tarball.")
211- self.build.updateStatus(BuildStatus.FULLYBUILT)
212 else:
213 tarball_file = open(filename)
214 try:
215@@ -129,9 +128,9 @@
216 self._uploadTarball(
217 self.build.buildqueue_record.specific_build.branch,
218 tarball, logger)
219+ transaction.commit()
220 logger.debug("Upload complete.")
221 finally:
222- self.build.updateStatus(BuildStatus.FULLYBUILT)
223 tarball_file.close()
224 os.remove(filename)
225- transaction.commit()
226+ defer.returnValue(BuildStatus.FULLYBUILT)