Merge lp:~julian-edwards/launchpad/async-file-uploads-bug-662631 into lp:launchpad/db-devel

Proposed by Julian Edwards
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 10021
Proposed branch: lp:~julian-edwards/launchpad/async-file-uploads-bug-662631
Merge into: lp:launchpad/db-devel
Diff against target: 1093 lines (+412/-233)
16 files modified
lib/lp/buildmaster/interfaces/builder.py (+1/-1)
lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py (+3/-0)
lib/lp/buildmaster/interfaces/packagebuild.py (+6/-1)
lib/lp/buildmaster/model/builder.py (+63/-37)
lib/lp/buildmaster/model/buildfarmjobbehavior.py (+2/-3)
lib/lp/buildmaster/model/packagebuild.py (+107/-84)
lib/lp/buildmaster/tests/mock_slaves.py (+20/-11)
lib/lp/buildmaster/tests/test_builder.py (+45/-1)
lib/lp/buildmaster/tests/test_packagebuild.py (+35/-28)
lib/lp/code/model/sourcepackagerecipebuild.py (+6/-4)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+3/-1)
lib/lp/soyuz/tests/test_binarypackagebuild.py (+3/-1)
lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py (+40/-31)
lib/lp/translations/model/translationtemplatesbuildbehavior.py (+47/-19)
lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py (+26/-10)
lib/lp_sitecustomize.py (+5/-1)
To merge this branch: bzr merge lp:~julian-edwards/launchpad/async-file-uploads-bug-662631
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Jonathan Lange (community) Abstain
Review via email: mp+40883@code.launchpad.net

Commit message

Make file uploads from builders asynchronous so that it doesn't block the rest of the build manager. This is the last part of the build manager to be made asynchronous.

Description of the change

This is the final part of the buildd-manager changes to make it fully asynchronous. Previously it was blocking when grabbing files from the builders - it now does that asynchronously using Twisted's HTTP file transfer.

Most of the changes are quite mechanical which accounts for most of the diff unfortunately - it's where I need to make the code an inner function which is called back from the Deferred. Hopefully you're familiar with this pattern; if not you'll learn it pretty quickly :)

Other changes are to add a new getFiles() function on the BuilderSlave object. This is initially complementing getFile() by calling it repeatedly but the longer term intention is to remove getFile() so that we can call one place and get hold of everything the builder knows about all at once.

The only other major change was a large amount of work to get the translations code fixed and tests passing. In particular, they were trying to test a successful tarball retrieval but not actually doing so - and in fact one test was asserting that nothing was retrieved!

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

I don't have time to review this properly, but after having a quick skim through it all looks good.

 <jml>bigjools: I don't think you need files_downloaded (in packagebuild.py) you could also do:

   d = slave.getFiles(filenames_to_download)
   d.addCallback(lambda x: self.storeBuildInfo(self, librarian, slave_status))
   d.addCallback(build_info_stored)
   return d

Is my only comment of any substance.

review: Abstain
Revision history for this message
Graham Binns (gmb) wrote :

One typo:

> 865 + # incompressed file in the librarian

Should be uncompressed.

Other than that I'm happy with this branch.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/buildmaster/interfaces/builder.py'
2--- lib/lp/buildmaster/interfaces/builder.py 2010-11-16 14:51:02 +0000
3+++ lib/lp/buildmaster/interfaces/builder.py 2010-11-30 11:46:52 +0000
4@@ -203,7 +203,7 @@
5 :param filename: The name of the file to be given to the librarian file
6 alias.
7 :param private: True if the build is for a private archive.
8- :return: A librarian file alias.
9+ :return: A Deferred that calls back with a librarian file alias.
10 """
11
12 def getBuildQueue():
13
14=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py'
15--- lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py 2010-04-12 05:52:01 +0000
16+++ lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py 2010-11-30 11:46:52 +0000
17@@ -72,5 +72,8 @@
18 """Verify the current build job status.
19
20 Perform the required actions for each state.
21+
22+ :param queueItem: The `BuildQueue` for the build.
23+ :return: A Deferred that fires when the update is done.
24 """
25
26
27=== modified file 'lib/lp/buildmaster/interfaces/packagebuild.py'
28--- lib/lp/buildmaster/interfaces/packagebuild.py 2010-10-14 20:41:13 +0000
29+++ lib/lp/buildmaster/interfaces/packagebuild.py 2010-11-30 11:46:52 +0000
30@@ -94,7 +94,11 @@
31 """
32
33 def getLogFromSlave(build):
34- """Get last buildlog from slave. """
35+ """Get last buildlog from slave.
36+
37+ :return: A Deferred that fires with the librarian ID of the log
38+ when the log is finished downloading.
39+ """
40
41 def estimateDuration():
42 """Estimate the build duration."""
43@@ -130,6 +134,7 @@
44
45 :param status: Slave build status string with 'BuildStatus.' stripped.
46 :param slave_status: A dict as returned by IBuilder.slaveStatus
47+ :return: A Deferred that fires when finished dealing with the build.
48 """
49
50 def queueBuild(suspended=False):
51
52=== modified file 'lib/lp/buildmaster/model/builder.py'
53--- lib/lp/buildmaster/model/builder.py 2010-11-26 18:36:09 +0000
54+++ lib/lp/buildmaster/model/builder.py 2010-11-30 11:46:52 +0000
55@@ -40,6 +40,7 @@
56 reactor as default_reactor,
57 )
58 from twisted.web import xmlrpc
59+from twisted.web.client import downloadPage
60
61 from zope.component import getUtility
62 from zope.interface import implements
63@@ -213,12 +214,35 @@
64 return self._with_timeout(self._server.callRemote(
65 'ensurepresent', sha1sum, url, username, password))
66
67- def getFile(self, sha_sum):
68- """Construct a file-like object to return the named file."""
69- # XXX 2010-10-18 bug=662631
70- # Change this to do non-blocking IO.
71- file_url = urlappend(self._file_cache_url, sha_sum)
72- return urllib2.urlopen(file_url)
73+ def getFile(self, sha_sum, file_to_write):
74+ """Fetch a file from the builder.
75+
76+ :param sha_sum: The sha of the file (which is also its name on the
77+ builder)
78+ :param file_to_write: A file name or file-like object to write
79+ the file to
80+ :return: A Deferred that calls back when the download is done, or
81+ errback with the error string.
82+ """
83+ file_url = urlappend(self._file_cache_url, sha_sum).encode('utf8')
84+ # If desired we can pass a param "timeout" here but let's leave
85+ # it at the default value if it becomes obvious we need to
86+ # change it.
87+ return downloadPage(file_url, file_to_write, followRedirect=0)
88+
89+ def getFiles(self, filemap):
90+ """Fetch many files from the builder.
91+
92+ :param filemap: A Dictionary containing key values of the builder
93+ file name to retrieve, which maps to a value containing the
94+ file name or file object to write the file to.
95+
96+ :return: A DeferredList that calls back when the download is done.
97+ """
98+ dl = defer.gatherResults([
99+ self.getFile(builder_file, filemap[builder_file])
100+ for builder_file in filemap])
101+ return dl
102
103 def resume(self, clock=None):
104 """Resume the builder in an asynchronous fashion.
105@@ -607,38 +631,40 @@
106 """See IBuilder."""
107 out_file_fd, out_file_name = tempfile.mkstemp(suffix=".buildlog")
108 out_file = os.fdopen(out_file_fd, "r+")
109- try:
110- # XXX 2010-10-18 bug=662631
111- # Change this to do non-blocking IO.
112- slave_file = self.slave.getFile(file_sha1)
113- copy_and_close(slave_file, out_file)
114- # If the requested file is the 'buildlog' compress it using gzip
115- # before storing in Librarian.
116- if file_sha1 == 'buildlog':
117+
118+ def got_file(ignored, filename, out_file, out_file_name):
119+ try:
120+ # If the requested file is the 'buildlog' compress it
121+ # using gzip before storing in Librarian.
122+ if file_sha1 == 'buildlog':
123+ out_file = open(out_file_name)
124+ filename += '.gz'
125+ out_file_name += '.gz'
126+ gz_file = gzip.GzipFile(out_file_name, mode='wb')
127+ copy_and_close(out_file, gz_file)
128+ os.remove(out_file_name.replace('.gz', ''))
129+
130+ # Reopen the file, seek to its end position, count and seek
131+ # to beginning, ready for adding to the Librarian.
132 out_file = open(out_file_name)
133- filename += '.gz'
134- out_file_name += '.gz'
135- gz_file = gzip.GzipFile(out_file_name, mode='wb')
136- copy_and_close(out_file, gz_file)
137- os.remove(out_file_name.replace('.gz', ''))
138-
139- # Reopen the file, seek to its end position, count and seek
140- # to beginning, ready for adding to the Librarian.
141- out_file = open(out_file_name)
142- out_file.seek(0, 2)
143- bytes_written = out_file.tell()
144- out_file.seek(0)
145-
146- library_file = getUtility(ILibraryFileAliasSet).create(
147- filename, bytes_written, out_file,
148- contentType=filenameToContentType(filename),
149- restricted=private)
150- finally:
151- # Finally, remove the temporary file
152- out_file.close()
153- os.remove(out_file_name)
154-
155- return library_file.id
156+ out_file.seek(0, 2)
157+ bytes_written = out_file.tell()
158+ out_file.seek(0)
159+
160+ library_file = getUtility(ILibraryFileAliasSet).create(
161+ filename, bytes_written, out_file,
162+ contentType=filenameToContentType(filename),
163+ restricted=private)
164+ finally:
165+ # Remove the temporary file. getFile() closes the file
166+ # object.
167+ os.remove(out_file_name)
168+
169+ return library_file.id
170+
171+ d = self.slave.getFile(file_sha1, out_file)
172+ d.addCallback(got_file, filename, out_file, out_file_name)
173+ return d
174
175 def isAvailable(self):
176 """See `IBuilder`."""
177
178=== modified file 'lib/lp/buildmaster/model/buildfarmjobbehavior.py'
179--- lib/lp/buildmaster/model/buildfarmjobbehavior.py 2010-10-27 14:25:19 +0000
180+++ lib/lp/buildmaster/model/buildfarmjobbehavior.py 2010-11-30 11:46:52 +0000
181@@ -191,9 +191,8 @@
182 # XXX: dsilvers 2005-03-02: Confirm the builder has the right build?
183
184 build = queueItem.specific_job.build
185- # XXX 2010-10-18 bug=662631
186- # Change this to do non-blocking IO.
187- build.handleStatus(build_status, librarian, slave_status)
188+ d = build.handleStatus(build_status, librarian, slave_status)
189+ return d
190
191
192 class IdleBuildBehavior(BuildFarmJobBehaviorBase):
193
194=== modified file 'lib/lp/buildmaster/model/packagebuild.py'
195--- lib/lp/buildmaster/model/packagebuild.py 2010-10-28 09:11:36 +0000
196+++ lib/lp/buildmaster/model/packagebuild.py 2010-11-30 11:46:52 +0000
197@@ -169,12 +169,11 @@
198 def getLogFromSlave(package_build):
199 """See `IPackageBuild`."""
200 builder = package_build.buildqueue_record.builder
201- # XXX 2010-10-18 bug=662631
202- # Change this to do non-blocking IO.
203- return builder.transferSlaveFileToLibrarian(
204+ d = builder.transferSlaveFileToLibrarian(
205 SLAVE_LOG_FILENAME,
206 package_build.buildqueue_record.getLogFileName(),
207 package_build.is_private)
208+ return d
209
210 def estimateDuration(self):
211 """See `IPackageBuild`."""
212@@ -183,21 +182,23 @@
213 @staticmethod
214 def storeBuildInfo(build, librarian, slave_status):
215 """See `IPackageBuild`."""
216- # log, builder and date_finished are read-only, so we must
217- # currently remove the security proxy to set them.
218- naked_build = removeSecurityProxy(build)
219- # XXX 2010-10-18 bug=662631
220- # Change this to do non-blocking IO.
221- naked_build.log = build.getLogFromSlave(build)
222- naked_build.builder = build.buildqueue_record.builder
223- # XXX cprov 20060615 bug=120584: Currently buildduration includes
224- # the scanner latency, it should really be asking the slave for
225- # the duration spent building locally.
226- naked_build.date_finished = datetime.datetime.now(pytz.UTC)
227- if slave_status.get('dependencies') is not None:
228- build.dependencies = unicode(slave_status.get('dependencies'))
229- else:
230- build.dependencies = None
231+ def got_log(lfa_id):
232+ # log, builder and date_finished are read-only, so we must
233+ # currently remove the security proxy to set them.
234+ naked_build = removeSecurityProxy(build)
235+ naked_build.log = lfa_id
236+ naked_build.builder = build.buildqueue_record.builder
237+ # XXX cprov 20060615 bug=120584: Currently buildduration includes
238+ # the scanner latency, it should really be asking the slave for
239+ # the duration spent building locally.
240+ naked_build.date_finished = datetime.datetime.now(pytz.UTC)
241+ if slave_status.get('dependencies') is not None:
242+ build.dependencies = unicode(slave_status.get('dependencies'))
243+ else:
244+ build.dependencies = None
245+
246+ d = build.getLogFromSlave(build)
247+ return d.addCallback(got_log)
248
249 def verifySuccessfulUpload(self):
250 """See `IPackageBuild`."""
251@@ -284,9 +285,8 @@
252 logger.critical("Unknown BuildStatus '%s' for builder '%s'"
253 % (status, self.buildqueue_record.builder.url))
254 return
255- # XXX 2010-10-18 bug=662631
256- # Change this to do non-blocking IO.
257- method(librarian, slave_status, logger)
258+ d = method(librarian, slave_status, logger)
259+ return d
260
261 def _handleStatus_OK(self, librarian, slave_status, logger):
262 """Handle a package that built successfully.
263@@ -326,6 +326,7 @@
264
265 slave = removeSecurityProxy(self.buildqueue_record.builder.slave)
266 successful_copy_from_slave = True
267+ filenames_to_download = {}
268 for filename in filemap:
269 logger.info("Grabbing file: %s" % filename)
270 out_file_name = os.path.join(upload_path, filename)
271@@ -338,46 +339,50 @@
272 "A slave tried to upload the file '%s' "
273 "for the build %d." % (filename, self.id))
274 break
275- out_file = open(out_file_name, "wb")
276- slave_file = slave.getFile(filemap[filename])
277- copy_and_close(slave_file, out_file)
278-
279+ filenames_to_download[filemap[filename]] = out_file_name
280+
281+ def build_info_stored(ignored):
282+ # We only attempt the upload if we successfully copied all the
283+ # files from the slave.
284+ if successful_copy_from_slave:
285+ logger.info(
286+ "Gathered %s %d completely. Moving %s to uploader queue." % (
287+ self.__class__.__name__, self.id, upload_leaf))
288+ target_dir = os.path.join(root, "incoming")
289+ self.status = BuildStatus.UPLOADING
290+ else:
291+ logger.warning(
292+ "Copy from slave for build %s was unsuccessful.", self.id)
293+ self.status = BuildStatus.FAILEDTOUPLOAD
294+ self.notify(extra_info='Copy from slave was unsuccessful.')
295+ target_dir = os.path.join(root, "failed")
296+
297+ if not os.path.exists(target_dir):
298+ os.mkdir(target_dir)
299+
300+ # Flush so there are no race conditions with archiveuploader about
301+ # self.status.
302+ Store.of(self).flush()
303+
304+ # Move the directory used to grab the binaries into
305+ # the incoming directory so the upload processor never
306+ # sees half-finished uploads.
307+ os.rename(grab_dir, os.path.join(target_dir, upload_leaf))
308+
309+ # Release the builder for another job.
310+ d = self.buildqueue_record.builder.cleanSlave()
311+
312+ # Remove BuildQueue record.
313+ return d.addCallback(
314+ lambda x:self.buildqueue_record.destroySelf())
315+
316+ d = slave.getFiles(filenames_to_download)
317 # Store build information, build record was already updated during
318 # the binary upload.
319- self.storeBuildInfo(self, librarian, slave_status)
320-
321- # We only attempt the upload if we successfully copied all the
322- # files from the slave.
323- if successful_copy_from_slave:
324- logger.info(
325- "Gathered %s %d completely. Moving %s to uploader queue." % (
326- self.__class__.__name__, self.id, upload_leaf))
327- target_dir = os.path.join(root, "incoming")
328- self.status = BuildStatus.UPLOADING
329- else:
330- logger.warning(
331- "Copy from slave for build %s was unsuccessful.", self.id)
332- self.status = BuildStatus.FAILEDTOUPLOAD
333- self.notify(extra_info='Copy from slave was unsuccessful.')
334- target_dir = os.path.join(root, "failed")
335-
336- if not os.path.exists(target_dir):
337- os.mkdir(target_dir)
338-
339- # Flush so there are no race conditions with archiveuploader about
340- # self.status.
341- Store.of(self).flush()
342-
343- # Move the directory used to grab the binaries into
344- # the incoming directory so the upload processor never
345- # sees half-finished uploads.
346- os.rename(grab_dir, os.path.join(target_dir, upload_leaf))
347-
348- # Release the builder for another job.
349- self.buildqueue_record.builder.cleanSlave()
350-
351- # Remove BuildQueue record.
352- self.buildqueue_record.destroySelf()
353+ d.addCallback(
354+ lambda x: self.storeBuildInfo(self, librarian, slave_status))
355+ d.addCallback(build_info_stored)
356+ return d
357
358 def _handleStatus_PACKAGEFAIL(self, librarian, slave_status, logger):
359 """Handle a package that had failed to build.
360@@ -387,10 +392,14 @@
361 remove Buildqueue entry.
362 """
363 self.status = BuildStatus.FAILEDTOBUILD
364- self.storeBuildInfo(self, librarian, slave_status)
365- self.buildqueue_record.builder.cleanSlave()
366- self.notify()
367- self.buildqueue_record.destroySelf()
368+ def build_info_stored(ignored):
369+ self.notify()
370+ d = self.buildqueue_record.builder.cleanSlave()
371+ return d.addCallback(
372+ lambda x:self.buildqueue_record.destroySelf())
373+
374+ d = self.storeBuildInfo(self, librarian, slave_status)
375+ return d.addCallback(build_info_stored)
376
377 def _handleStatus_DEPFAIL(self, librarian, slave_status, logger):
378 """Handle a package that had missing dependencies.
379@@ -400,11 +409,15 @@
380 entry and release builder slave for another job.
381 """
382 self.status = BuildStatus.MANUALDEPWAIT
383- self.storeBuildInfo(self, librarian, slave_status)
384- logger.critical("***** %s is MANUALDEPWAIT *****"
385- % self.buildqueue_record.builder.name)
386- self.buildqueue_record.builder.cleanSlave()
387- self.buildqueue_record.destroySelf()
388+ def build_info_stored(ignored):
389+ logger.critical("***** %s is MANUALDEPWAIT *****"
390+ % self.buildqueue_record.builder.name)
391+ d = self.buildqueue_record.builder.cleanSlave()
392+ return d.addCallback(
393+ lambda x:self.buildqueue_record.destroySelf())
394+
395+ d = self.storeBuildInfo(self, librarian, slave_status)
396+ return d.addCallback(build_info_stored)
397
398 def _handleStatus_CHROOTFAIL(self, librarian, slave_status, logger):
399 """Handle a package that had failed when unpacking the CHROOT.
400@@ -414,12 +427,16 @@
401 and release the builder.
402 """
403 self.status = BuildStatus.CHROOTWAIT
404- self.storeBuildInfo(self, librarian, slave_status)
405- logger.critical("***** %s is CHROOTWAIT *****" %
406- self.buildqueue_record.builder.name)
407- self.buildqueue_record.builder.cleanSlave()
408- self.notify()
409- self.buildqueue_record.destroySelf()
410+ def build_info_stored(ignored):
411+ logger.critical("***** %s is CHROOTWAIT *****" %
412+ self.buildqueue_record.builder.name)
413+ self.notify()
414+ d = self.buildqueue_record.builder.cleanSlave()
415+ return d.addCallback(
416+ lambda x:self.buildqueue_record.destroySelf())
417+
418+ d = self.storeBuildInfo(self, librarian, slave_status)
419+ return d.addCallback(build_info_stored)
420
421 def _handleStatus_BUILDERFAIL(self, librarian, slave_status, logger):
422 """Handle builder failures.
423@@ -432,9 +449,11 @@
424 % self.buildqueue_record.builder.name)
425 self.buildqueue_record.builder.failBuilder(
426 "Builder returned BUILDERFAIL when asked for its status")
427- # simply reset job
428- self.storeBuildInfo(self, librarian, slave_status)
429- self.buildqueue_record.reset()
430+ def build_info_stored(ignored):
431+ # simply reset job
432+ self.buildqueue_record.reset()
433+ d = self.storeBuildInfo(self, librarian, slave_status)
434+ return d.addCallback(build_info_stored)
435
436 def _handleStatus_GIVENBACK(self, librarian, slave_status, logger):
437 """Handle automatic retry requested by builder.
438@@ -446,13 +465,17 @@
439 logger.warning("***** %s is GIVENBACK by %s *****"
440 % (self.buildqueue_record.specific_job.build.title,
441 self.buildqueue_record.builder.name))
442- self.storeBuildInfo(self, librarian, slave_status)
443- # XXX cprov 2006-05-30: Currently this information is not
444- # properly presented in the Web UI. We will discuss it in
445- # the next Paris Summit, infinity has some ideas about how
446- # to use this content. For now we just ensure it's stored.
447- self.buildqueue_record.builder.cleanSlave()
448- self.buildqueue_record.reset()
449+ def build_info_stored(ignored):
450+ # XXX cprov 2006-05-30: Currently this information is not
451+ # properly presented in the Web UI. We will discuss it in
452+ # the next Paris Summit, infinity has some ideas about how
453+ # to use this content. For now we just ensure it's stored.
454+ d = self.buildqueue_record.builder.cleanSlave()
455+ self.buildqueue_record.reset()
456+ return d
457+
458+ d = self.storeBuildInfo(self, librarian, slave_status)
459+ return d.addCallback(build_info_stored)
460
461
462 class PackageBuildSet:
463
464=== modified file 'lib/lp/buildmaster/tests/mock_slaves.py'
465--- lib/lp/buildmaster/tests/mock_slaves.py 2010-11-12 12:54:42 +0000
466+++ lib/lp/buildmaster/tests/mock_slaves.py 2010-11-30 11:46:52 +0000
467@@ -23,6 +23,7 @@
468
469 import fixtures
470 import os
471+import types
472
473 from StringIO import StringIO
474 import xmlrpclib
475@@ -161,6 +162,12 @@
476 return self.sendFileToSlave(
477 libraryfilealias.content.sha1, libraryfilealias.http_url)
478
479+ def getFiles(self, filemap):
480+ dl = defer.gatherResults([
481+ self.getFile(builder_file, filemap[builder_file])
482+ for builder_file in filemap])
483+ return dl
484+
485
486 class BuildingSlave(OkSlave):
487 """A mock slave that looks like it's currently building."""
488@@ -175,13 +182,14 @@
489 return defer.succeed(
490 ('BuilderStatus.BUILDING', self.build_id, buildlog))
491
492- def getFile(self, sum):
493- # XXX: This needs to be updated to return a Deferred.
494+ def getFile(self, sum, file_to_write):
495 self.call_log.append('getFile')
496 if sum == "buildlog":
497- s = StringIO("This is a build log")
498- s.headers = {'content-length': 19}
499- return s
500+ if isinstance(file_to_write, types.StringTypes):
501+ file_to_write = open(file_to_write, 'wb')
502+ file_to_write.write("This is a build log")
503+ file_to_write.close()
504+ return defer.succeed(None)
505
506
507 class WaitingSlave(OkSlave):
508@@ -208,14 +216,15 @@
509 'BuilderStatus.WAITING', self.state, self.build_id, self.filemap,
510 self.dependencies))
511
512- def getFile(self, hash):
513- # XXX: This needs to be updated to return a Deferred.
514+ def getFile(self, hash, file_to_write):
515 self.call_log.append('getFile')
516 if hash in self.valid_file_hashes:
517 content = "This is a %s" % hash
518- s = StringIO(content)
519- s.headers = {'content-length': len(content)}
520- return s
521+ if isinstance(file_to_write, types.StringTypes):
522+ file_to_write = open(file_to_write, 'wb')
523+ file_to_write.write(content)
524+ file_to_write.close()
525+ return defer.succeed(None)
526
527
528 class AbortingSlave(OkSlave):
529@@ -314,7 +323,7 @@
530 Points to a fixed URL that is also used by `BuilddSlaveTestSetup`.
531 """
532 return BuilderSlave.makeBuilderSlave(
533- self.TEST_URL, 'vmhost', reactor, proxy)
534+ self.BASE_URL, 'vmhost', reactor, proxy)
535
536 def makeCacheFile(self, tachandler, filename):
537 """Make a cache file available on the remote slave.
538
539=== modified file 'lib/lp/buildmaster/tests/test_builder.py'
540--- lib/lp/buildmaster/tests/test_builder.py 2010-11-26 18:36:09 +0000
541+++ lib/lp/buildmaster/tests/test_builder.py 2010-11-30 11:46:52 +0000
542@@ -5,6 +5,7 @@
543
544 import os
545 import signal
546+import tempfile
547 import xmlrpclib
548
549 from testtools.deferredruntest import (
550@@ -14,7 +15,10 @@
551 SynchronousDeferredRunTest,
552 )
553
554-from twisted.internet.defer import CancelledError
555+from twisted.internet.defer import (
556+ CancelledError,
557+ DeferredList,
558+ )
559 from twisted.internet.task import Clock
560 from twisted.python.failure import Failure
561 from twisted.web.client import getPage
562@@ -1150,3 +1154,43 @@
563 d = getPage(expected_url.encode('utf8'))
564 return d.addCallback(self.assertEqual, content)
565 return d.addCallback(check_file)
566+
567+ def test_getFiles(self):
568+ # Test BuilderSlave.getFiles().
569+ # It also implicitly tests getFile() - I don't want to test that
570+ # separately because it increases test run time and it's going
571+ # away at some point anyway, in favour of getFiles().
572+ contents = ["content1", "content2", "content3"]
573+ self.slave_helper.getServerSlave()
574+ slave = self.slave_helper.getClientSlave()
575+ filemap = {}
576+ content_map = {}
577+
578+ def got_files(ignored):
579+ # Called back when getFiles finishes. Make sure all the
580+ # content is as expected.
581+ got_contents = []
582+ for sha1 in filemap:
583+ local_file = filemap[sha1]
584+ file = open(local_file)
585+ self.assertEqual(content_map[sha1], file.read())
586+ file.close()
587+
588+ def finished_uploading(ignored):
589+ d = slave.getFiles(filemap)
590+ return d.addCallback(got_files)
591+
592+ # Set up some files on the builder and store details in
593+ # content_map so we can compare downloads later.
594+ dl = []
595+ for content in contents:
596+ filename = content + '.txt'
597+ lf = self.factory.makeLibraryFileAlias(filename, content=content)
598+ content_map[lf.content.sha1] = content
599+ fd, filemap[lf.content.sha1] = tempfile.mkstemp()
600+ self.addCleanup(os.remove, filemap[lf.content.sha1])
601+ self.layer.txn.commit()
602+ d = slave.ensurepresent(lf.content.sha1, lf.http_url, "", "")
603+ dl.append(d)
604+
605+ return DeferredList(dl).addCallback(finished_uploading)
606
607=== modified file 'lib/lp/buildmaster/tests/test_packagebuild.py'
608--- lib/lp/buildmaster/tests/test_packagebuild.py 2010-10-28 09:11:36 +0000
609+++ lib/lp/buildmaster/tests/test_packagebuild.py 2010-11-30 11:46:52 +0000
610@@ -8,6 +8,7 @@
611 from datetime import datetime
612 import hashlib
613 import os
614+import shutil
615
616 from storm.store import Store
617 from zope.component import getUtility
618@@ -43,6 +44,7 @@
619 login_person,
620 TestCaseWithFactory,
621 )
622+from lp.testing.factory import LaunchpadObjectFactory
623 from lp.testing.fakemethod import FakeMethod
624
625
626@@ -98,8 +100,6 @@
627 self.assertRaises(
628 NotImplementedError, self.package_build.verifySuccessfulUpload)
629 self.assertRaises(NotImplementedError, self.package_build.notify)
630- # XXX 2010-10-18 bug=662631
631- # Change this to do non-blocking IO.
632 self.assertRaises(
633 NotImplementedError, self.package_build.handleStatus,
634 None, None, None)
635@@ -283,6 +283,7 @@
636 class TestHandleStatusMixin:
637 """Tests for `IPackageBuild`s handleStatus method.
638
639+ This should be run with a Trial TestCase.
640 """
641
642 layer = LaunchpadZopelessLayer
643@@ -293,6 +294,7 @@
644
645 def setUp(self):
646 super(TestHandleStatusMixin, self).setUp()
647+ self.factory = LaunchpadObjectFactory()
648 self.build = self.makeBuild()
649 # For the moment, we require a builder for the build so that
650 # handleStatus_OK can get a reference to the slave.
651@@ -304,7 +306,10 @@
652 builder.setSlaveForTesting(self.slave)
653
654 # We overwrite the buildmaster root to use a temp directory.
655- self.upload_root = self.makeTemporaryDirectory()
656+ tempdir = self.mktemp()
657+ os.mkdir(tempdir)
658+ self.addCleanup(shutil.rmtree, tempdir)
659+ self.upload_root = tempdir
660 tmp_builddmaster_root = """
661 [builddmaster]
662 root: %s
663@@ -325,56 +330,58 @@
664 # A filemap with plain filenames should not cause a problem.
665 # The call to handleStatus will attempt to get the file from
666 # the slave resulting in a URL error in this test case.
667- # XXX 2010-10-18 bug=662631
668- # Change this to do non-blocking IO.
669- self.build.handleStatus('OK', None, {
670+ def got_status(ignored):
671+ self.assertEqual(BuildStatus.UPLOADING, self.build.status)
672+ self.assertResultCount(1, "incoming")
673+
674+ d = self.build.handleStatus('OK', None, {
675 'filemap': {'myfile.py': 'test_file_hash'},
676 })
677-
678- self.assertEqual(BuildStatus.UPLOADING, self.build.status)
679- self.assertResultCount(1, "incoming")
680+ return d.addCallback(got_status)
681
682 def test_handleStatus_OK_absolute_filepath(self):
683 # A filemap that tries to write to files outside of
684 # the upload directory will result in a failed upload.
685- # XXX 2010-10-18 bug=662631
686- # Change this to do non-blocking IO.
687- self.build.handleStatus('OK', None, {
688+ def got_status(ignored):
689+ self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.status)
690+ self.assertResultCount(0, "failed")
691+ self.assertIdentical(None, self.build.buildqueue_record)
692+
693+ d = self.build.handleStatus('OK', None, {
694 'filemap': {'/tmp/myfile.py': 'test_file_hash'},
695 })
696- self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.status)
697- self.assertResultCount(0, "failed")
698- self.assertIs(None, self.build.buildqueue_record)
699+ return d.addCallback(got_status)
700
701 def test_handleStatus_OK_relative_filepath(self):
702 # A filemap that tries to write to files outside of
703 # the upload directory will result in a failed upload.
704- # XXX 2010-10-18 bug=662631
705- # Change this to do non-blocking IO.
706- self.build.handleStatus('OK', None, {
707+ def got_status(ignored):
708+ self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.status)
709+ self.assertResultCount(0, "failed")
710+
711+ d = self.build.handleStatus('OK', None, {
712 'filemap': {'../myfile.py': 'test_file_hash'},
713 })
714- self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.status)
715- self.assertResultCount(0, "failed")
716+ return d.addCallback(got_status)
717
718 def test_handleStatus_OK_sets_build_log(self):
719 # The build log is set during handleStatus.
720 removeSecurityProxy(self.build).log = None
721 self.assertEqual(None, self.build.log)
722- # XXX 2010-10-18 bug=662631
723- # Change this to do non-blocking IO.
724- self.build.handleStatus('OK', None, {
725+ d = self.build.handleStatus('OK', None, {
726 'filemap': {'myfile.py': 'test_file_hash'},
727 })
728- self.assertNotEqual(None, self.build.log)
729+ def got_status(ignored):
730+ self.assertNotEqual(None, self.build.log)
731+ return d.addCallback(got_status)
732
733 def test_date_finished_set(self):
734 # The date finished is updated during handleStatus_OK.
735 removeSecurityProxy(self.build).date_finished = None
736 self.assertEqual(None, self.build.date_finished)
737- # XXX 2010-10-18 bug=662631
738- # Change this to do non-blocking IO.
739- self.build.handleStatus('OK', None, {
740+ d = self.build.handleStatus('OK', None, {
741 'filemap': {'myfile.py': 'test_file_hash'},
742 })
743- self.assertNotEqual(None, self.build.date_finished)
744+ def got_status(ignored):
745+ self.assertNotEqual(None, self.build.date_finished)
746+ return d.addCallback(got_status)
747
748=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
749--- lib/lp/code/model/sourcepackagerecipebuild.py 2010-11-11 20:56:21 +0000
750+++ lib/lp/code/model/sourcepackagerecipebuild.py 2010-11-30 11:46:52 +0000
751@@ -312,11 +312,13 @@
752
753 def _handleStatus_OK(self, librarian, slave_status, logger):
754 """See `IPackageBuild`."""
755- super(SourcePackageRecipeBuild, self)._handleStatus_OK(
756+ d = super(SourcePackageRecipeBuild, self)._handleStatus_OK(
757 librarian, slave_status, logger)
758- # base implementation doesn't notify on success.
759- if self.status == BuildStatus.FULLYBUILT:
760- self.notify()
761+ def uploaded_build(ignored):
762+ # Base implementation doesn't notify on success.
763+ if self.status == BuildStatus.FULLYBUILT:
764+ self.notify()
765+ return d.addCallback(uploaded_build)
766
767 def getUploader(self, changes):
768 """See `IPackageBuild`."""
769
770=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
771--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-10-27 14:20:21 +0000
772+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-11-30 11:46:52 +0000
773@@ -14,6 +14,8 @@
774 from zope.component import getUtility
775 from zope.security.proxy import removeSecurityProxy
776
777+from twisted.trial.unittest import TestCase as TrialTestCase
778+
779 from canonical.launchpad.interfaces.lpstorm import IStore
780 from canonical.launchpad.webapp.authorization import check_permission
781 from canonical.launchpad.webapp.testing import verifyObject
782@@ -424,7 +426,7 @@
783
784
785 class TestHandleStatusForSPRBuild(
786- MakeSPRecipeBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
787+ MakeSPRecipeBuildMixin, TestHandleStatusMixin, TrialTestCase):
788 """IPackageBuild.handleStatus works with SPRecipe builds."""
789
790
791
792=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py'
793--- lib/lp/soyuz/tests/test_binarypackagebuild.py 2010-10-06 11:46:51 +0000
794+++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2010-11-30 11:46:52 +0000
795@@ -13,6 +13,8 @@
796 from zope.component import getUtility
797 from zope.security.proxy import removeSecurityProxy
798
799+from twisted.trial.unittest import TestCase as TrialTestCase
800+
801 from canonical.testing.layers import LaunchpadZopelessLayer
802 from lp.buildmaster.enums import BuildStatus
803 from lp.buildmaster.interfaces.builder import IBuilderSet
804@@ -466,5 +468,5 @@
805
806
807 class TestHandleStatusForBinaryPackageBuild(
808- MakeBinaryPackageBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
809+ MakeBinaryPackageBuildMixin, TestHandleStatusMixin, TrialTestCase):
810 """IPackageBuild.handleStatus works with binary builds."""
811
812=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py'
813--- lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py 2010-11-25 17:37:34 +0000
814+++ lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py 2010-11-30 11:46:52 +0000
815@@ -435,37 +435,46 @@
816 self.build.status = BuildStatus.FULLYBUILT
817 old_tmps = sorted(os.listdir('/tmp'))
818
819- # Grabbing logs should not leave new files in /tmp (bug #172798)
820- # XXX 2010-10-18 bug=662631
821- # Change this to do non-blocking IO.
822- logfile_lfa_id = self.build.getLogFromSlave(self.build)
823- logfile_lfa = getUtility(ILibraryFileAliasSet)[logfile_lfa_id]
824- new_tmps = sorted(os.listdir('/tmp'))
825- self.assertEqual(old_tmps, new_tmps)
826-
827- # The new librarian file is stored compressed with a .gz
828- # extension and text/plain file type for easy viewing in
829- # browsers, as it decompresses and displays the file inline.
830- self.assertTrue(logfile_lfa.filename.endswith('_FULLYBUILT.txt.gz'))
831- self.assertEqual('text/plain', logfile_lfa.mimetype)
832- self.layer.txn.commit()
833-
834- # LibrarianFileAlias does not implement tell() or seek(), which
835- # are required by gzip.open(), so we need to read the file out
836- # of the librarian first.
837- fd, fname = tempfile.mkstemp()
838- self.addCleanup(os.remove, fname)
839- tmp = os.fdopen(fd, 'wb')
840- tmp.write(logfile_lfa.read())
841- tmp.close()
842- uncompressed_file = gzip.open(fname).read()
843-
844- # XXX: 2010-10-18 bug=662631
845- # When the mock slave is changed to return a Deferred,
846- # update this test too.
847- orig_file = removeSecurityProxy(self.builder.slave).getFile(
848- 'buildlog').read()
849- self.assertEqual(orig_file, uncompressed_file)
850+ def got_log(logfile_lfa_id):
851+ # Grabbing logs should not leave new files in /tmp (bug #172798)
852+ logfile_lfa = getUtility(ILibraryFileAliasSet)[logfile_lfa_id]
853+ new_tmps = sorted(os.listdir('/tmp'))
854+ self.assertEqual(old_tmps, new_tmps)
855+
856+ # The new librarian file is stored compressed with a .gz
857+ # extension and text/plain file type for easy viewing in
858+ # browsers, as it decompresses and displays the file inline.
859+ self.assertTrue(
860+ logfile_lfa.filename.endswith('_FULLYBUILT.txt.gz'))
861+ self.assertEqual('text/plain', logfile_lfa.mimetype)
862+ self.layer.txn.commit()
863+
864+ # LibrarianFileAlias does not implement tell() or seek(), which
865+ # are required by gzip.open(), so we need to read the file out
866+ # of the librarian first.
867+ fd, fname = tempfile.mkstemp()
868+ self.addCleanup(os.remove, fname)
869+ tmp = os.fdopen(fd, 'wb')
870+ tmp.write(logfile_lfa.read())
871+ tmp.close()
872+ uncompressed_file = gzip.open(fname).read()
873+
874+ # Now make a temp filename that getFile() can write to.
875+ fd, tmp_orig_file_name = tempfile.mkstemp()
876+ self.addCleanup(os.remove, tmp_orig_file_name)
877+
878+ # Check that the original file from the slave matches the
879+ # uncompressed file in the librarian.
880+ def got_orig_log(ignored):
881+ orig_file_content = open(tmp_orig_file_name).read()
882+ self.assertEqual(orig_file_content, uncompressed_file)
883+
884+ d = removeSecurityProxy(self.builder.slave).getFile(
885+ 'buildlog', tmp_orig_file_name)
886+ return d.addCallback(got_orig_log)
887+
888+ d = self.build.getLogFromSlave(self.build)
889+ return d.addCallback(got_log)
890
891 def test_private_build_log_storage(self):
892 # Builds in private archives should have their log uploaded to
893
894=== modified file 'lib/lp/translations/model/translationtemplatesbuildbehavior.py'
895--- lib/lp/translations/model/translationtemplatesbuildbehavior.py 2010-10-27 14:25:19 +0000
896+++ lib/lp/translations/model/translationtemplatesbuildbehavior.py 2010-11-30 11:46:52 +0000
897@@ -11,6 +11,11 @@
898 'TranslationTemplatesBuildBehavior',
899 ]
900
901+import os
902+import tempfile
903+
904+from twisted.internet import defer
905+
906 from zope.component import getUtility
907 from zope.interface import implements
908 from zope.security.proxy import removeSecurityProxy
909@@ -74,17 +79,20 @@
910 """Read tarball with generated translation templates from slave."""
911 if filemap is None:
912 logger.error("Slave returned no filemap.")
913- return None
914+ return defer.succeed(None)
915
916 slave_filename = filemap.get(self.templates_tarball_path)
917 if slave_filename is None:
918 logger.error("Did not find templates tarball in slave output.")
919- return None
920+ return defer.succeed(None)
921
922 slave = removeSecurityProxy(buildqueue.builder.slave)
923- # XXX 2010-10-18 bug=662631
924- # Change this to do non-blocking IO.
925- return slave.getFile(slave_filename).read()
926+
927+ fd, fname = tempfile.mkstemp()
928+ tarball_file = os.fdopen(fd, 'wb')
929+ d = slave.getFile(slave_filename, tarball_file)
930+ # getFile will close the file object.
931+ return d.addCallback(lambda ignored: fname)
932
933 def _uploadTarball(self, branch, tarball, logger):
934 """Upload tarball to productseries that want it."""
935@@ -120,20 +128,40 @@
936 queue_item.specific_job.branch.bzr_identity,
937 build_status))
938
939+ def clean_slave(ignored):
940+ d = queue_item.builder.cleanSlave()
941+ return d.addCallback(lambda ignored: queue_item.destroySelf())
942+
943+ def got_tarball(filename):
944+ # XXX 2010-11-12 bug=674575
945+ # Please make addOrUpdateEntriesFromTarball() take files on
946+ # disk; reading arbitrarily sized files into memory is
947+ # dangerous.
948+ if filename is None:
949+ logger.error("Build produced no tarball.")
950+ return
951+
952+ tarball_file = open(filename)
953+ try:
954+ tarball = tarball_file.read()
955+ if tarball is None:
956+ logger.error("Build produced empty tarball.")
957+ else:
958+ logger.debug("Uploading translation templates tarball.")
959+ self._uploadTarball(
960+ queue_item.specific_job.branch, tarball, logger)
961+ logger.debug("Upload complete.")
962+ finally:
963+ tarball_file.close()
964+ os.remove(filename)
965+
966 if build_status == 'OK':
967 logger.debug("Processing successful templates build.")
968 filemap = slave_status.get('filemap')
969- # XXX 2010-10-18 bug=662631
970- # Change this to do non-blocking IO.
971- tarball = self._readTarball(queue_item, filemap, logger)
972-
973- if tarball is None:
974- logger.error("Build produced no tarball.")
975- else:
976- logger.debug("Uploading translation templates tarball.")
977- self._uploadTarball(
978- queue_item.specific_job.branch, tarball, logger)
979- logger.debug("Upload complete.")
980-
981- d = queue_item.builder.cleanSlave()
982- return d.addCallback(lambda ignored: queue_item.destroySelf())
983+ d = self._readTarball(queue_item, filemap, logger)
984+ d.addCallback(got_tarball)
985+ d.addCallback(clean_slave)
986+ return d
987+
988+ return clean_slave(None)
989+
990
991=== modified file 'lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py'
992--- lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py 2010-11-09 00:04:11 +0000
993+++ lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py 2010-11-30 11:46:52 +0000
994@@ -14,9 +14,12 @@
995 from zope.component import getUtility
996 from zope.security.proxy import removeSecurityProxy
997
998+from twisted.internet import defer
999+
1000 from canonical.config import config
1001 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
1002 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
1003+from canonical.librarian.utils import copy_and_close
1004 from canonical.testing.layers import (
1005 LaunchpadZopelessLayer,
1006 )
1007@@ -166,9 +169,17 @@
1008 path = behavior.templates_tarball_path
1009 # Poke the file we're expecting into the mock slave.
1010 behavior._builder.slave.valid_file_hashes.append(path)
1011- self.assertEqual(
1012- "This is a %s" % path,
1013- behavior._readTarball(buildqueue, {path: path}, logging))
1014+ def got_tarball(filename):
1015+ tarball = open(filename, 'r')
1016+ try:
1017+ self.assertEqual(
1018+ "This is a %s" % path, tarball.read())
1019+ finally:
1020+ tarball.close()
1021+ os.remove(filename)
1022+
1023+ d = behavior._readTarball(buildqueue, {path: path}, logging)
1024+ return d.addCallback(got_tarball)
1025
1026 def test_updateBuild_WAITING_OK(self):
1027 # Hopefully, a build will succeed and produce a tarball.
1028@@ -190,8 +201,10 @@
1029 def got_status(status):
1030 slave_call_log = behavior._builder.slave.call_log
1031 slave_status = {
1032- 'builder_status': status[0], 'build_status': status[1]}
1033- behavior.updateSlaveStatus(status, slave_status)
1034+ 'builder_status': status[0],
1035+ 'build_status': status[1],
1036+ 'filemap': {'translation-templates.tar.gz': 'foo'},
1037+ }
1038 return behavior.updateBuild_WAITING(
1039 queue_item, slave_status, None, logging), slave_call_log
1040
1041@@ -199,7 +212,7 @@
1042 slave_call_log = behavior._builder.slave.call_log
1043 self.assertEqual(1, queue_item.destroySelf.call_count)
1044 self.assertIn('clean', slave_call_log)
1045- self.assertEqual(0, behavior._uploadTarball.call_count)
1046+ self.assertEqual(1, behavior._uploadTarball.call_count)
1047
1048 d.addCallback(got_dispatch)
1049 d.addCallback(got_status)
1050@@ -292,12 +305,15 @@
1051
1052 d = behavior.dispatchBuildToSlave(queue_item, logging)
1053
1054- def got_dispatch((status, info)):
1055+ def fake_getFile(sum, file):
1056 dummy_tar = os.path.join(
1057 os.path.dirname(__file__), 'dummy_templates.tar.gz')
1058- # XXX 2010-10-18 bug=662631
1059- # Change this to do non-blocking IO.
1060- builder.slave.getFile = lambda sum: open(dummy_tar)
1061+ tar_file = open(dummy_tar)
1062+ copy_and_close(tar_file, file)
1063+ return defer.succeed(None)
1064+
1065+ def got_dispatch((status, info)):
1066+ builder.slave.getFile = fake_getFile
1067 builder.slave.filemap = {
1068 'translation-templates.tar.gz': 'foo'}
1069 return builder.slave.status()
1070
1071=== modified file 'lib/lp_sitecustomize.py'
1072--- lib/lp_sitecustomize.py 2010-10-27 14:25:19 +0000
1073+++ lib/lp_sitecustomize.py 2010-11-30 11:46:52 +0000
1074@@ -9,7 +9,10 @@
1075 import warnings
1076 import logging
1077
1078-from twisted.internet.defer import Deferred
1079+from twisted.internet.defer import (
1080+ Deferred,
1081+ DeferredList,
1082+ )
1083
1084 from bzrlib.branch import Branch
1085 from lp.services.log import loglevels
1086@@ -105,6 +108,7 @@
1087 customizeMimetypes()
1088 dont_wrap_class_and_subclasses(Branch)
1089 checker.BasicTypes.update({Deferred: checker.NoProxy})
1090+ checker.BasicTypes.update({DeferredList: checker.NoProxy})
1091 checker.BasicTypes[itertools.groupby] = checker._iteratorChecker
1092 # The itertools._grouper type is not exposed by name, so we must get it
1093 # through actually using itertools.groupby.

Subscribers

People subscribed via source and target branches

to status/vote changes: