Merge lp:~allenap/launchpad/handle-status-ro-crash-bug-905853 into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 14641
Proposed branch: lp:~allenap/launchpad/handle-status-ro-crash-bug-905853
Merge into: lp:launchpad
Diff against target: 1657 lines (+555/-227)
16 files modified
buildout-templates/bin/retest.in (+11/-2)
lib/lp/archiveuploader/tests/test_uploadprocessor.py (+4/-2)
lib/lp/buildmaster/interfaces/builder.py (+3/-1)
lib/lp/buildmaster/manager.py (+74/-46)
lib/lp/buildmaster/model/builder.py (+28/-15)
lib/lp/buildmaster/model/buildfarmjobbehavior.py (+63/-32)
lib/lp/buildmaster/model/packagebuild.py (+94/-54)
lib/lp/buildmaster/testing.py (+59/-0)
lib/lp/buildmaster/tests/test_builder.py (+25/-2)
lib/lp/buildmaster/tests/test_manager.py (+120/-29)
lib/lp/buildmaster/tests/test_packagebuild.py (+18/-16)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+6/-8)
lib/lp/services/database/tests/test_transaction_policy.py (+14/-4)
lib/lp/services/database/transaction_policy.py (+16/-4)
lib/lp/soyuz/tests/test_binarypackagebuild.py (+7/-4)
lib/lp/translations/model/translationtemplatesbuildbehavior.py (+13/-8)
To merge this branch: bzr merge lp:~allenap/launchpad/handle-status-ro-crash-bug-905853
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Julian Edwards (community) Approve
Review via email: mp+86298@code.launchpad.net

Commit message

[r=gmb,julian-edwards][bug=905853] In buildmaster, always shift into a read-write database transaction access mode before updating PackageBuild statuses.

Description of the change

This fixes the problem described in the bug, but it also fixes the tests to (a) run correctly (i.e. with AsynchronousDeferredRunTest) and (b) run with a read-only transaction access mode by default. Combined, these brought out the problems described in the bug. This branch also (c) changes DatabaseTransactionPolicy.__enter__() to only care about open transactions when we're moving from a read-write access mode to read-only.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

<bigjools> allenap: in the tests in your branch, it's probably worth refactoring the bit that sets properties on objects in a r/w transaction
<allenap> bigjools: Erm, which bit?
<allenap> bigjools: Like in test_handleStatus_OK_sets_build_log?
<bigjools> allenap: line 72/83 of the diff
<bigjools> allenap: I suspect we'll need to do that a lot more in the future
<allenap> bigjools: I don't know what a better way would be. I could instead enter read-only mode in each test individually (via a fixture) I guess.
<bigjools> allenap: I was thinking just a test helper
<bigjools> like setattr
<bigjools> but does the whole transactionny thing
<allenap> bigjools: With the removeSecurityProxy thing too I assume.
<bigjools> allenap: no, the caller can do that
<allenap> bigjools: Okay, I think I have a cool way to do that.

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

Nice branch; I've no complaints.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'buildout-templates/bin/retest.in'
--- buildout-templates/bin/retest.in 2011-12-22 14:35:39 +0000
+++ buildout-templates/bin/retest.in 2012-01-03 12:26:30 +0000
@@ -28,7 +28,7 @@
28import os28import os
29import re29import re
30import sys30import sys
31from itertools import takewhile31from itertools import takewhile, imap
3232
33${python-relative-path-setup}33${python-relative-path-setup}
3434
@@ -38,6 +38,14 @@
38# Regular expression to match numbered stories.38# Regular expression to match numbered stories.
39STORY_RE = re.compile("(.*)/\d{2}-.*")39STORY_RE = re.compile("(.*)/\d{2}-.*")
4040
41# Regular expression to remove terminal color escapes.
42COLOR_RE = re.compile("\x1b[[][0-9;]+m")
43
44
45def decolorize(text):
46 """Remove all ANSI terminal color escapes from `text`."""
47 return COLOR_RE.sub("", text)
48
4149
42def get_test_name(test):50def get_test_name(test):
43 """Get the test name of a failed test.51 """Get the test name of a failed test.
@@ -91,7 +99,8 @@
9199
92100
93if __name__ == '__main__':101if __name__ == '__main__':
94 tests = extract_tests(fileinput.input())102 lines = imap(decolorize, fileinput.input())
103 tests = extract_tests(lines)
95 if len(tests) >= 1:104 if len(tests) >= 1:
96 run_tests(tests)105 run_tests(tests)
97 else:106 else:
98107
=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2011-12-30 06:14:56 +0000
+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2012-01-03 12:26:30 +0000
@@ -629,7 +629,8 @@
629 from_addr, to_addrs, raw_msg = stub.test_emails.pop()629 from_addr, to_addrs, raw_msg = stub.test_emails.pop()
630 foo_bar = "Foo Bar <foo.bar@canonical.com>"630 foo_bar = "Foo Bar <foo.bar@canonical.com>"
631 daniel = "Daniel Silverstone <daniel.silverstone@canonical.com>"631 daniel = "Daniel Silverstone <daniel.silverstone@canonical.com>"
632 self.assertEqual([e.strip() for e in to_addrs], [foo_bar, daniel])632 self.assertContentEqual(
633 [foo_bar, daniel], [e.strip() for e in to_addrs])
633 self.assertTrue(634 self.assertTrue(
634 "NEW" in raw_msg, "Expected email containing 'NEW', got:\n%s"635 "NEW" in raw_msg, "Expected email containing 'NEW', got:\n%s"
635 % raw_msg)636 % raw_msg)
@@ -663,7 +664,8 @@
663 from_addr, to_addrs, raw_msg = stub.test_emails.pop()664 from_addr, to_addrs, raw_msg = stub.test_emails.pop()
664 daniel = "Daniel Silverstone <daniel.silverstone@canonical.com>"665 daniel = "Daniel Silverstone <daniel.silverstone@canonical.com>"
665 foo_bar = "Foo Bar <foo.bar@canonical.com>"666 foo_bar = "Foo Bar <foo.bar@canonical.com>"
666 self.assertEqual([e.strip() for e in to_addrs], [foo_bar, daniel])667 self.assertContentEqual(
668 [foo_bar, daniel], [e.strip() for e in to_addrs])
667 self.assertTrue("Waiting for approval" in raw_msg,669 self.assertTrue("Waiting for approval" in raw_msg,
668 "Expected an 'upload awaits approval' email.\n"670 "Expected an 'upload awaits approval' email.\n"
669 "Got:\n%s" % raw_msg)671 "Got:\n%s" % raw_msg)
670672
=== modified file 'lib/lp/buildmaster/interfaces/builder.py'
--- lib/lp/buildmaster/interfaces/builder.py 2012-01-01 02:58:52 +0000
+++ lib/lp/buildmaster/interfaces/builder.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# pylint: disable-msg=E0211,E02134# pylint: disable-msg=E0211,E0213
@@ -195,6 +195,8 @@
195195
196 def setSlaveForTesting(proxy):196 def setSlaveForTesting(proxy):
197 """Sets the RPC proxy through which to operate the build slave."""197 """Sets the RPC proxy through which to operate the build slave."""
198 # XXX JeroenVermeulen 2011-11-09, bug=888010: Don't use this.
199 # It's a trap. See bug for details.
198200
199 def verifySlaveBuildCookie(slave_build_id):201 def verifySlaveBuildCookie(slave_build_id):
200 """Verify that a slave's build cookie is consistent.202 """Verify that a slave's build cookie is consistent.
201203
=== modified file 'lib/lp/buildmaster/manager.py'
--- lib/lp/buildmaster/manager.py 2012-01-01 02:58:52 +0000
+++ lib/lp/buildmaster/manager.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Soyuz buildd slave manager logic."""4"""Soyuz buildd slave manager logic."""
@@ -34,6 +34,7 @@
34 BuildBehaviorMismatch,34 BuildBehaviorMismatch,
35 )35 )
36from lp.buildmaster.model.builder import Builder36from lp.buildmaster.model.builder import Builder
37from lp.services.database.transaction_policy import DatabaseTransactionPolicy
3738
3839
39BUILDD_MANAGER_LOG_NAME = "slave-scanner"40BUILDD_MANAGER_LOG_NAME = "slave-scanner"
@@ -111,13 +112,17 @@
111 # algorithm for polling.112 # algorithm for polling.
112 SCAN_INTERVAL = 15113 SCAN_INTERVAL = 15
113114
114 def __init__(self, builder_name, logger):115 def __init__(self, builder_name, logger, clock=None):
115 self.builder_name = builder_name116 self.builder_name = builder_name
116 self.logger = logger117 self.logger = logger
118 if clock is None:
119 clock = reactor
120 self._clock = clock
117121
118 def startCycle(self):122 def startCycle(self):
119 """Scan the builder and dispatch to it or deal with failures."""123 """Scan the builder and dispatch to it or deal with failures."""
120 self.loop = LoopingCall(self.singleCycle)124 self.loop = LoopingCall(self.singleCycle)
125 self.loop.clock = self._clock
121 self.stopping_deferred = self.loop.start(self.SCAN_INTERVAL)126 self.stopping_deferred = self.loop.start(self.SCAN_INTERVAL)
122 return self.stopping_deferred127 return self.stopping_deferred
123128
@@ -138,51 +143,58 @@
138 1. Print the error in the log143 1. Print the error in the log
139 2. Increment and assess failure counts on the builder and job.144 2. Increment and assess failure counts on the builder and job.
140 """145 """
141 # Make sure that pending database updates are removed as it146 # Since this is a failure path, we could be in a broken
142 # could leave the database in an inconsistent state (e.g. The147 # transaction. Get us a fresh one.
143 # job says it's running but the buildqueue has no builder set).
144 transaction.abort()148 transaction.abort()
145149
146 # If we don't recognise the exception include a stack trace with150 # If we don't recognise the exception include a stack trace with
147 # the error.151 # the error.
148 error_message = failure.getErrorMessage()152 error_message = failure.getErrorMessage()
149 if failure.check(153 familiar_error = failure.check(
150 BuildSlaveFailure, CannotBuild, BuildBehaviorMismatch,154 BuildSlaveFailure, CannotBuild, BuildBehaviorMismatch,
151 CannotResumeHost, BuildDaemonError, CannotFetchFile):155 CannotResumeHost, BuildDaemonError, CannotFetchFile)
152 self.logger.info("Scanning %s failed with: %s" % (156 if familiar_error:
153 self.builder_name, error_message))157 self.logger.info(
158 "Scanning %s failed with: %s",
159 self.builder_name, error_message)
154 else:160 else:
155 self.logger.info("Scanning %s failed with: %s\n%s" % (161 self.logger.info(
162 "Scanning %s failed with: %s\n%s",
156 self.builder_name, failure.getErrorMessage(),163 self.builder_name, failure.getErrorMessage(),
157 failure.getTraceback()))164 failure.getTraceback())
158165
159 # Decide if we need to terminate the job or fail the166 # Decide if we need to terminate the job or fail the
160 # builder.167 # builder.
161 try:168 try:
162 builder = get_builder(self.builder_name)169 builder = get_builder(self.builder_name)
163 builder.gotFailure()170 transaction.commit()
164 if builder.currentjob is not None:171
165 build_farm_job = builder.getCurrentBuildFarmJob()172 with DatabaseTransactionPolicy(read_only=False):
166 build_farm_job.gotFailure()173 builder.gotFailure()
167 self.logger.info(174
168 "builder %s failure count: %s, "175 if builder.currentjob is None:
169 "job '%s' failure count: %s" % (176 self.logger.info(
177 "Builder %s failed a probe, count: %s",
178 self.builder_name, builder.failure_count)
179 else:
180 build_farm_job = builder.getCurrentBuildFarmJob()
181 build_farm_job.gotFailure()
182 self.logger.info(
183 "builder %s failure count: %s, "
184 "job '%s' failure count: %s",
170 self.builder_name,185 self.builder_name,
171 builder.failure_count,186 builder.failure_count,
172 build_farm_job.title,187 build_farm_job.title,
173 build_farm_job.failure_count))188 build_farm_job.failure_count)
174 else:189
175 self.logger.info(190 assessFailureCounts(builder, failure.getErrorMessage())
176 "Builder %s failed a probe, count: %s" % (191 transaction.commit()
177 self.builder_name, builder.failure_count))
178 assessFailureCounts(builder, failure.getErrorMessage())
179 transaction.commit()
180 except:192 except:
181 # Catastrophic code failure! Not much we can do.193 # Catastrophic code failure! Not much we can do.
194 transaction.abort()
182 self.logger.error(195 self.logger.error(
183 "Miserable failure when trying to examine failure counts:\n",196 "Miserable failure when trying to examine failure counts:\n",
184 exc_info=True)197 exc_info=True)
185 transaction.abort()
186198
187 def checkCancellation(self, builder):199 def checkCancellation(self, builder):
188 """See if there is a pending cancellation request.200 """See if there is a pending cancellation request.
@@ -236,14 +248,9 @@
236 """248 """
237 # We need to re-fetch the builder object on each cycle as the249 # We need to re-fetch the builder object on each cycle as the
238 # Storm store is invalidated over transaction boundaries.250 # Storm store is invalidated over transaction boundaries.
239
240 self.builder = get_builder(self.builder_name)251 self.builder = get_builder(self.builder_name)
241252
242 def status_updated(ignored):253 def status_updated(ignored):
243 # Commit the changes done while possibly rescuing jobs, to
244 # avoid holding table locks.
245 transaction.commit()
246
247 # See if we think there's an active build on the builder.254 # See if we think there's an active build on the builder.
248 buildqueue = self.builder.getBuildQueue()255 buildqueue = self.builder.getBuildQueue()
249256
@@ -253,14 +260,10 @@
253 return self.builder.updateBuild(buildqueue)260 return self.builder.updateBuild(buildqueue)
254261
255 def build_updated(ignored):262 def build_updated(ignored):
256 # Commit changes done while updating the build, to avoid
257 # holding table locks.
258 transaction.commit()
259
260 # If the builder is in manual mode, don't dispatch anything.263 # If the builder is in manual mode, don't dispatch anything.
261 if self.builder.manual:264 if self.builder.manual:
262 self.logger.debug(265 self.logger.debug(
263 '%s is in manual mode, not dispatching.' %266 '%s is in manual mode, not dispatching.',
264 self.builder.name)267 self.builder.name)
265 return268 return
266269
@@ -278,22 +281,33 @@
278 job = self.builder.currentjob281 job = self.builder.currentjob
279 if job is not None and not self.builder.builderok:282 if job is not None and not self.builder.builderok:
280 self.logger.info(283 self.logger.info(
281 "%s was made unavailable, resetting attached "284 "%s was made unavailable; resetting attached job.",
282 "job" % self.builder.name)285 self.builder.name)
283 job.reset()
284 transaction.commit()286 transaction.commit()
287 with DatabaseTransactionPolicy(read_only=False):
288 job.reset()
289 transaction.commit()
285 return290 return
286291
287 # See if there is a job we can dispatch to the builder slave.292 # See if there is a job we can dispatch to the builder slave.
288293
294 # XXX JeroenVermeulen 2011-10-11, bug=872112: The job's
295 # failure count will be reset once the job has started
296 # successfully. Because of intervening commits, you may see
297 # a build with a nonzero failure count that's actually going
298 # to succeed later (and have a failure count of zero). Or
299 # it may fail yet end up with a lower failure count than you
300 # saw earlier.
289 d = self.builder.findAndStartJob()301 d = self.builder.findAndStartJob()
290302
291 def job_started(candidate):303 def job_started(candidate):
292 if self.builder.currentjob is not None:304 if self.builder.currentjob is not None:
293 # After a successful dispatch we can reset the305 # After a successful dispatch we can reset the
294 # failure_count.306 # failure_count.
295 self.builder.resetFailureCount()
296 transaction.commit()307 transaction.commit()
308 with DatabaseTransactionPolicy(read_only=False):
309 self.builder.resetFailureCount()
310 transaction.commit()
297 return self.builder.slave311 return self.builder.slave
298 else:312 else:
299 return None313 return None
@@ -372,6 +386,7 @@
372 self.logger = self._setupLogger()386 self.logger = self._setupLogger()
373 self.new_builders_scanner = NewBuildersScanner(387 self.new_builders_scanner = NewBuildersScanner(
374 manager=self, clock=clock)388 manager=self, clock=clock)
389 self.transaction_policy = DatabaseTransactionPolicy(read_only=True)
375390
376 def _setupLogger(self):391 def _setupLogger(self):
377 """Set up a 'slave-scanner' logger that redirects to twisted.392 """Set up a 'slave-scanner' logger that redirects to twisted.
@@ -390,16 +405,28 @@
390 logger.setLevel(level)405 logger.setLevel(level)
391 return logger406 return logger
392407
408 def enterReadOnlyDatabasePolicy(self):
409 """Set the database transaction policy to read-only.
410
411 Any previously pending changes are committed first.
412 """
413 transaction.commit()
414 self.transaction_policy.__enter__()
415
416 def exitReadOnlyDatabasePolicy(self, *args):
417 """Reset database transaction policy to the default read-write."""
418 self.transaction_policy.__exit__(None, None, None)
419
393 def startService(self):420 def startService(self):
394 """Service entry point, called when the application starts."""421 """Service entry point, called when the application starts."""
422 # Avoiding circular imports.
423 from lp.buildmaster.interfaces.builder import IBuilderSet
424
425 self.enterReadOnlyDatabasePolicy()
395426
396 # Get a list of builders and set up scanners on each one.427 # Get a list of builders and set up scanners on each one.
397428 self.addScanForBuilders(
398 # Avoiding circular imports.429 [builder.name for builder in getUtility(IBuilderSet)])
399 from lp.buildmaster.interfaces.builder import IBuilderSet
400 builder_set = getUtility(IBuilderSet)
401 builders = [builder.name for builder in builder_set]
402 self.addScanForBuilders(builders)
403 self.new_builders_scanner.scheduleScan()430 self.new_builders_scanner.scheduleScan()
404431
405 # Events will now fire in the SlaveScanner objects to scan each432 # Events will now fire in the SlaveScanner objects to scan each
@@ -420,6 +447,7 @@
420 # stopped, so we can wait on them all at once here before447 # stopped, so we can wait on them all at once here before
421 # exiting.448 # exiting.
422 d = defer.DeferredList(deferreds, consumeErrors=True)449 d = defer.DeferredList(deferreds, consumeErrors=True)
450 d.addCallback(self.exitReadOnlyDatabasePolicy)
423 return d451 return d
424452
425 def addScanForBuilders(self, builders):453 def addScanForBuilders(self, builders):
426454
=== modified file 'lib/lp/buildmaster/model/builder.py'
--- lib/lp/buildmaster/model/builder.py 2012-01-02 11:21:14 +0000
+++ lib/lp/buildmaster/model/builder.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009,2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# pylint: disable-msg=E0611,W02124# pylint: disable-msg=E0611,W0212
@@ -66,6 +66,7 @@
66 SQLBase,66 SQLBase,
67 sqlvalues,67 sqlvalues,
68 )68 )
69from lp.services.database.transaction_policy import DatabaseTransactionPolicy
69from lp.services.helpers import filenameToContentType70from lp.services.helpers import filenameToContentType
70from lp.services.job.interfaces.job import JobStatus71from lp.services.job.interfaces.job import JobStatus
71from lp.services.job.model.job import Job72from lp.services.job.model.job import Job
@@ -545,6 +546,8 @@
545546
546 def setSlaveForTesting(self, proxy):547 def setSlaveForTesting(self, proxy):
547 """See IBuilder."""548 """See IBuilder."""
549 # XXX JeroenVermeulen 2011-11-09, bug=888010: Don't use this.
550 # It's a trap. See bug for details.
548 self._testing_slave = proxy551 self._testing_slave = proxy
549 del get_property_cache(self).slave552 del get_property_cache(self).slave
550553
@@ -673,10 +676,13 @@
673 bytes_written = out_file.tell()676 bytes_written = out_file.tell()
674 out_file.seek(0)677 out_file.seek(0)
675678
676 library_file = getUtility(ILibraryFileAliasSet).create(679 transaction.commit()
677 filename, bytes_written, out_file,680 with DatabaseTransactionPolicy(read_only=False):
678 contentType=filenameToContentType(filename),681 library_file = getUtility(ILibraryFileAliasSet).create(
679 restricted=private)682 filename, bytes_written, out_file,
683 contentType=filenameToContentType(filename),
684 restricted=private)
685 transaction.commit()
680 finally:686 finally:
681 # Remove the temporary file. getFile() closes the file687 # Remove the temporary file. getFile() closes the file
682 # object.688 # object.
@@ -714,7 +720,7 @@
714 def acquireBuildCandidate(self):720 def acquireBuildCandidate(self):
715 """Acquire a build candidate in an atomic fashion.721 """Acquire a build candidate in an atomic fashion.
716722
717 When retrieiving a candidate we need to mark it as building723 When retrieving a candidate we need to mark it as building
718 immediately so that it is not dispatched by another builder in the724 immediately so that it is not dispatched by another builder in the
719 build manager.725 build manager.
720726
@@ -724,12 +730,15 @@
724 can be in this code at the same time.730 can be in this code at the same time.
725731
726 If there's ever more than one build manager running at once, then732 If there's ever more than one build manager running at once, then
727 this code will need some sort of mutex.733 this code will need some sort of mutex, or run in a single
734 transaction.
728 """735 """
729 candidate = self._findBuildCandidate()736 candidate = self._findBuildCandidate()
730 if candidate is not None:737 if candidate is not None:
731 candidate.markAsBuilding(self)
732 transaction.commit()738 transaction.commit()
739 with DatabaseTransactionPolicy(read_only=False):
740 candidate.markAsBuilding(self)
741 transaction.commit()
733 return candidate742 return candidate
734743
735 def _findBuildCandidate(self):744 def _findBuildCandidate(self):
@@ -792,13 +801,17 @@
792 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)801 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
793 candidate_jobs = store.execute(query).get_all()802 candidate_jobs = store.execute(query).get_all()
794803
795 for (candidate_id,) in candidate_jobs:804 transaction.commit()
796 candidate = getUtility(IBuildQueueSet).get(candidate_id)805 with DatabaseTransactionPolicy(read_only=False):
797 job_class = job_classes[candidate.job_type]806 for (candidate_id,) in candidate_jobs:
798 candidate_approved = job_class.postprocessCandidate(807 candidate = getUtility(IBuildQueueSet).get(candidate_id)
799 candidate, logger)808 job_class = job_classes[candidate.job_type]
800 if candidate_approved:809 candidate_approved = job_class.postprocessCandidate(
801 return candidate810 candidate, logger)
811 if candidate_approved:
812 transaction.commit()
813 return candidate
814 transaction.commit()
802815
803 return None816 return None
804817
805818
=== modified file 'lib/lp/buildmaster/model/buildfarmjobbehavior.py'
--- lib/lp/buildmaster/model/buildfarmjobbehavior.py 2011-12-30 02:24:09 +0000
+++ lib/lp/buildmaster/model/buildfarmjobbehavior.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# pylint: disable-msg=E0211,E02134# pylint: disable-msg=E0211,E0213
@@ -16,6 +16,7 @@
16import socket16import socket
17import xmlrpclib17import xmlrpclib
1818
19import transaction
19from twisted.internet import defer20from twisted.internet import defer
20from zope.component import getUtility21from zope.component import getUtility
21from zope.interface import implements22from zope.interface import implements
@@ -30,6 +31,7 @@
30 IBuildFarmJobBehavior,31 IBuildFarmJobBehavior,
31 )32 )
32from lp.services import encoding33from lp.services import encoding
34from lp.services.database.transaction_policy import DatabaseTransactionPolicy
33from lp.services.job.interfaces.job import JobStatus35from lp.services.job.interfaces.job import JobStatus
34from lp.services.librarian.interfaces.client import ILibrarianClient36from lp.services.librarian.interfaces.client import ILibrarianClient
3537
@@ -69,6 +71,25 @@
69 if slave_build_cookie != expected_cookie:71 if slave_build_cookie != expected_cookie:
70 raise CorruptBuildCookie("Invalid slave build cookie.")72 raise CorruptBuildCookie("Invalid slave build cookie.")
7173
74 def _getBuilderStatusHandler(self, status_text, logger):
75 """Look up the handler method for a given builder status.
76
77 If status is not a known one, logs an error and returns None.
78 """
79 builder_status_handlers = {
80 'BuilderStatus.IDLE': self.updateBuild_IDLE,
81 'BuilderStatus.BUILDING': self.updateBuild_BUILDING,
82 'BuilderStatus.ABORTING': self.updateBuild_ABORTING,
83 'BuilderStatus.ABORTED': self.updateBuild_ABORTED,
84 'BuilderStatus.WAITING': self.updateBuild_WAITING,
85 }
86 handler = builder_status_handlers.get(status_text)
87 if handler is None:
88 logger.critical(
89 "Builder on %s returned unknown status %s; failing it.",
90 self._builder.url, status_text)
91 return handler
92
72 def updateBuild(self, queueItem):93 def updateBuild(self, queueItem):
73 """See `IBuildFarmJobBehavior`."""94 """See `IBuildFarmJobBehavior`."""
74 logger = logging.getLogger('slave-scanner')95 logger = logging.getLogger('slave-scanner')
@@ -76,6 +97,7 @@
76 d = self._builder.slaveStatus()97 d = self._builder.slaveStatus()
7798
78 def got_failure(failure):99 def got_failure(failure):
100 transaction.abort()
79 failure.trap(xmlrpclib.Fault, socket.error)101 failure.trap(xmlrpclib.Fault, socket.error)
80 info = failure.value102 info = failure.value
81 info = ("Could not contact the builder %s, caught a (%s)"103 info = ("Could not contact the builder %s, caught a (%s)"
@@ -83,27 +105,22 @@
83 raise BuildSlaveFailure(info)105 raise BuildSlaveFailure(info)
84106
85 def got_status(slave_status):107 def got_status(slave_status):
86 builder_status_handlers = {
87 'BuilderStatus.IDLE': self.updateBuild_IDLE,
88 'BuilderStatus.BUILDING': self.updateBuild_BUILDING,
89 'BuilderStatus.ABORTING': self.updateBuild_ABORTING,
90 'BuilderStatus.ABORTED': self.updateBuild_ABORTED,
91 'BuilderStatus.WAITING': self.updateBuild_WAITING,
92 }
93
94 builder_status = slave_status['builder_status']108 builder_status = slave_status['builder_status']
95 if builder_status not in builder_status_handlers:109 status_handler = self._getBuilderStatusHandler(
96 logger.critical(110 builder_status, logger)
97 "Builder on %s returned unknown status %s, failing it"111 if status_handler is None:
98 % (self._builder.url, builder_status))112 error = (
99 self._builder.failBuilder(
100 "Unknown status code (%s) returned from status() probe."113 "Unknown status code (%s) returned from status() probe."
101 % builder_status)114 % builder_status)
102 # XXX: This will leave the build and job in a bad state, but115 transaction.commit()
103 # should never be possible, since our builder statuses are116 with DatabaseTransactionPolicy(read_only=False):
104 # known.117 self._builder.failBuilder(error)
105 queueItem._builder = None118 # XXX: This will leave the build and job in a bad
106 queueItem.setDateStarted(None)119 # state, but should never be possible since our
120 # builder statuses are known.
121 queueItem._builder = None
122 queueItem.setDateStarted(None)
123 transaction.commit()
107 return124 return
108125
109 # Since logtail is a xmlrpclib.Binary container and it is126 # Since logtail is a xmlrpclib.Binary container and it is
@@ -113,9 +130,8 @@
113 # will simply remove the proxy.130 # will simply remove the proxy.
114 logtail = removeSecurityProxy(slave_status.get('logtail'))131 logtail = removeSecurityProxy(slave_status.get('logtail'))
115132
116 method = builder_status_handlers[builder_status]
117 return defer.maybeDeferred(133 return defer.maybeDeferred(
118 method, queueItem, slave_status, logtail, logger)134 status_handler, queueItem, slave_status, logtail, logger)
119135
120 d.addErrback(got_failure)136 d.addErrback(got_failure)
121 d.addCallback(got_status)137 d.addCallback(got_status)
@@ -127,22 +143,32 @@
127 Log this and reset the record.143 Log this and reset the record.
128 """144 """
129 logger.warn(145 logger.warn(
130 "Builder %s forgot about buildqueue %d -- resetting buildqueue "146 "Builder %s forgot about buildqueue %d -- "
131 "record" % (queueItem.builder.url, queueItem.id))147 "resetting buildqueue record.",
132 queueItem.reset()148 queueItem.builder.url, queueItem.id)
149 transaction.commit()
150 with DatabaseTransactionPolicy(read_only=False):
151 queueItem.reset()
152 transaction.commit()
133153
134 def updateBuild_BUILDING(self, queueItem, slave_status, logtail, logger):154 def updateBuild_BUILDING(self, queueItem, slave_status, logtail, logger):
135 """Build still building, collect the logtail"""155 """Build still building, collect the logtail"""
136 if queueItem.job.status != JobStatus.RUNNING:156 transaction.commit()
137 queueItem.job.start()157 with DatabaseTransactionPolicy(read_only=False):
138 queueItem.logtail = encoding.guess(str(logtail))158 if queueItem.job.status != JobStatus.RUNNING:
159 queueItem.job.start()
160 queueItem.logtail = encoding.guess(str(logtail))
161 transaction.commit()
139162
140 def updateBuild_ABORTING(self, queueItem, slave_status, logtail, logger):163 def updateBuild_ABORTING(self, queueItem, slave_status, logtail, logger):
141 """Build was ABORTED.164 """Build was ABORTED.
142165
143 Master-side should wait until the slave finish the process correctly.166 Master-side should wait until the slave finish the process correctly.
144 """167 """
145 queueItem.logtail = "Waiting for slave process to be terminated"168 transaction.commit()
169 with DatabaseTransactionPolicy(read_only=False):
170 queueItem.logtail = "Waiting for slave process to be terminated"
171 transaction.commit()
146172
147 def updateBuild_ABORTED(self, queueItem, slave_status, logtail, logger):173 def updateBuild_ABORTED(self, queueItem, slave_status, logtail, logger):
148 """ABORTING process has successfully terminated.174 """ABORTING process has successfully terminated.
@@ -150,11 +176,16 @@
150 Clean the builder for another jobs.176 Clean the builder for another jobs.
151 """177 """
152 d = queueItem.builder.cleanSlave()178 d = queueItem.builder.cleanSlave()
179
153 def got_cleaned(ignored):180 def got_cleaned(ignored):
154 queueItem.builder = None181 transaction.commit()
155 if queueItem.job.status != JobStatus.FAILED:182 with DatabaseTransactionPolicy(read_only=False):
156 queueItem.job.fail()183 queueItem.builder = None
157 queueItem.specific_job.jobAborted()184 if queueItem.job.status != JobStatus.FAILED:
185 queueItem.job.fail()
186 queueItem.specific_job.jobAborted()
187 transaction.commit()
188
158 return d.addCallback(got_cleaned)189 return d.addCallback(got_cleaned)
159190
160 def extractBuildStatus(self, slave_status):191 def extractBuildStatus(self, slave_status):
161192
=== modified file 'lib/lp/buildmaster/model/packagebuild.py'
--- lib/lp/buildmaster/model/packagebuild.py 2011-12-30 06:14:56 +0000
+++ lib/lp/buildmaster/model/packagebuild.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -24,6 +24,7 @@
24 Storm,24 Storm,
25 Unicode,25 Unicode,
26 )26 )
27import transaction
27from zope.component import getUtility28from zope.component import getUtility
28from zope.interface import (29from zope.interface import (
29 classProvides,30 classProvides,
@@ -50,6 +51,7 @@
50from lp.services.config import config51from lp.services.config import config
51from lp.services.database.enumcol import DBEnum52from lp.services.database.enumcol import DBEnum
52from lp.services.database.lpstorm import IMasterStore53from lp.services.database.lpstorm import IMasterStore
54from lp.services.database.transaction_policy import DatabaseTransactionPolicy
53from lp.services.helpers import filenameToContentType55from lp.services.helpers import filenameToContentType
54from lp.services.librarian.browser import ProxiedLibraryFileAlias56from lp.services.librarian.browser import ProxiedLibraryFileAlias
55from lp.services.librarian.interfaces import ILibraryFileAliasSet57from lp.services.librarian.interfaces import ILibraryFileAliasSet
@@ -177,19 +179,24 @@
177 def storeBuildInfo(build, librarian, slave_status):179 def storeBuildInfo(build, librarian, slave_status):
178 """See `IPackageBuild`."""180 """See `IPackageBuild`."""
179 def got_log(lfa_id):181 def got_log(lfa_id):
182 dependencies = slave_status.get('dependencies')
183 if dependencies is not None:
184 dependencies = unicode(dependencies)
185
180 # log, builder and date_finished are read-only, so we must186 # log, builder and date_finished are read-only, so we must
181 # currently remove the security proxy to set them.187 # currently remove the security proxy to set them.
182 naked_build = removeSecurityProxy(build)188 naked_build = removeSecurityProxy(build)
183 naked_build.log = lfa_id189
184 naked_build.builder = build.buildqueue_record.builder190 transaction.commit()
185 # XXX cprov 20060615 bug=120584: Currently buildduration includes191 with DatabaseTransactionPolicy(read_only=False):
186 # the scanner latency, it should really be asking the slave for192 naked_build.log = lfa_id
187 # the duration spent building locally.193 naked_build.builder = build.buildqueue_record.builder
188 naked_build.date_finished = datetime.datetime.now(pytz.UTC)194 # XXX cprov 20060615 bug=120584: Currently buildduration
189 if slave_status.get('dependencies') is not None:195 # includes the scanner latency. It should really be asking
190 build.dependencies = unicode(slave_status.get('dependencies'))196 # the slave for the duration spent building locally.
191 else:197 naked_build.date_finished = datetime.datetime.now(pytz.UTC)
192 build.dependencies = None198 build.dependencies = dependencies
199 transaction.commit()
193200
194 d = build.getLogFromSlave(build)201 d = build.getLogFromSlave(build)
195 return d.addCallback(got_log)202 return d.addCallback(got_log)
@@ -290,22 +297,41 @@
290297
291 def handleStatus(self, status, librarian, slave_status):298 def handleStatus(self, status, librarian, slave_status):
292 """See `IPackageBuild`."""299 """See `IPackageBuild`."""
300 # Avoid circular imports.
293 from lp.buildmaster.manager import BUILDD_MANAGER_LOG_NAME301 from lp.buildmaster.manager import BUILDD_MANAGER_LOG_NAME
302
294 logger = logging.getLogger(BUILDD_MANAGER_LOG_NAME)303 logger = logging.getLogger(BUILDD_MANAGER_LOG_NAME)
295 send_notification = status in self.ALLOWED_STATUS_NOTIFICATIONS304 send_notification = status in self.ALLOWED_STATUS_NOTIFICATIONS
296 method = getattr(self, '_handleStatus_' + status, None)305 method = getattr(self, '_handleStatus_' + status, None)
297 if method is None:306 if method is None:
298 logger.critical("Unknown BuildStatus '%s' for builder '%s'"307 logger.critical(
299 % (status, self.buildqueue_record.builder.url))308 "Unknown BuildStatus '%s' for builder '%s'",
300 return309 status, self.buildqueue_record.builder.url)
310 return None
311
301 d = method(librarian, slave_status, logger, send_notification)312 d = method(librarian, slave_status, logger, send_notification)
302 return d313 return d
303314
315 def _destroy_buildqueue_record(self, unused_arg):
316 """Destroy this build's `BuildQueue` record."""
317 transaction.commit()
318 with DatabaseTransactionPolicy(read_only=False):
319 self.buildqueue_record.destroySelf()
320 transaction.commit()
321
304 def _release_builder_and_remove_queue_item(self):322 def _release_builder_and_remove_queue_item(self):
305 # Release the builder for another job.323 # Release the builder for another job.
306 d = self.buildqueue_record.builder.cleanSlave()324 d = self.buildqueue_record.builder.cleanSlave()
307 # Remove BuildQueue record.325 # Remove BuildQueue record.
308 return d.addCallback(lambda x: self.buildqueue_record.destroySelf())326 return d.addCallback(self._destroy_buildqueue_record)
327
328 def _notify_if_appropriate(self, appropriate=True, extra_info=None):
329 """If `appropriate`, call `self.notify` in a write transaction."""
330 if appropriate:
331 transaction.commit()
332 with DatabaseTransactionPolicy(read_only=False):
333 self.notify(extra_info=extra_info)
334 transaction.commit()
309335
310 def _handleStatus_OK(self, librarian, slave_status, logger,336 def _handleStatus_OK(self, librarian, slave_status, logger,
311 send_notification):337 send_notification):
@@ -321,16 +347,19 @@
321 self.buildqueue_record.specific_job.build.title,347 self.buildqueue_record.specific_job.build.title,
322 self.buildqueue_record.builder.name))348 self.buildqueue_record.builder.name))
323349
324 # If this is a binary package build, discard it if its source is350 # If this is a binary package build for a source that is no
325 # no longer published.351 # longer published, discard it.
326 if self.build_farm_job_type == BuildFarmJobType.PACKAGEBUILD:352 if self.build_farm_job_type == BuildFarmJobType.PACKAGEBUILD:
327 build = self.buildqueue_record.specific_job.build353 build = self.buildqueue_record.specific_job.build
328 if not build.current_source_publication:354 if not build.current_source_publication:
329 build.status = BuildStatus.SUPERSEDED355 transaction.commit()
356 with DatabaseTransactionPolicy(read_only=False):
357 build.status = BuildStatus.SUPERSEDED
358 transaction.commit()
330 return self._release_builder_and_remove_queue_item()359 return self._release_builder_and_remove_queue_item()
331360
332 # Explode before collect a binary that is denied in this361 # Explode rather than collect a binary that is denied in this
333 # distroseries/pocket362 # distroseries/pocket.
334 if not self.archive.allowUpdatesToReleasePocket():363 if not self.archive.allowUpdatesToReleasePocket():
335 assert self.distro_series.canUploadToPocket(self.pocket), (364 assert self.distro_series.canUploadToPocket(self.pocket), (
336 "%s (%s) can not be built for pocket %s: illegal status"365 "%s (%s) can not be built for pocket %s: illegal status"
@@ -375,18 +404,26 @@
375 # files from the slave.404 # files from the slave.
376 if successful_copy_from_slave:405 if successful_copy_from_slave:
377 logger.info(406 logger.info(
378 "Gathered %s %d completely. Moving %s to uploader queue."407 "Gathered %s %d completely. "
379 % (self.__class__.__name__, self.id, upload_leaf))408 "Moving %s to uploader queue.",
409 self.__class__.__name__, self.id, upload_leaf)
380 target_dir = os.path.join(root, "incoming")410 target_dir = os.path.join(root, "incoming")
381 self.status = BuildStatus.UPLOADING411 resulting_status = BuildStatus.UPLOADING
382 else:412 else:
383 logger.warning(413 logger.warning(
384 "Copy from slave for build %s was unsuccessful.", self.id)414 "Copy from slave for build %s was unsuccessful.",
385 self.status = BuildStatus.FAILEDTOUPLOAD415 self.id)
386 if send_notification:
387 self.notify(
388 extra_info='Copy from slave was unsuccessful.')
389 target_dir = os.path.join(root, "failed")416 target_dir = os.path.join(root, "failed")
417 resulting_status = BuildStatus.FAILEDTOUPLOAD
418
419 transaction.commit()
420 with DatabaseTransactionPolicy(read_only=False):
421 self.status = resulting_status
422 transaction.commit()
423
424 if not successful_copy_from_slave:
425 self._notify_if_appropriate(
426 send_notification, "Copy from slave was unsuccessful.")
390427
391 if not os.path.exists(target_dir):428 if not os.path.exists(target_dir):
392 os.mkdir(target_dir)429 os.mkdir(target_dir)
@@ -394,10 +431,6 @@
394 # Release the builder for another job.431 # Release the builder for another job.
395 d = self._release_builder_and_remove_queue_item()432 d = self._release_builder_and_remove_queue_item()
396433
397 # Commit so there are no race conditions with archiveuploader
398 # about self.status.
399 Store.of(self).commit()
400
401 # Move the directory used to grab the binaries into434 # Move the directory used to grab the binaries into
402 # the incoming directory so the upload processor never435 # the incoming directory so the upload processor never
403 # sees half-finished uploads.436 # sees half-finished uploads.
@@ -421,14 +454,15 @@
421 set the job status as FAILEDTOBUILD, store available info and454 set the job status as FAILEDTOBUILD, store available info and
422 remove Buildqueue entry.455 remove Buildqueue entry.
423 """456 """
424 self.status = BuildStatus.FAILEDTOBUILD457 transaction.commit()
458 with DatabaseTransactionPolicy(read_only=False):
459 self.status = BuildStatus.FAILEDTOBUILD
460 transaction.commit()
425461
426 def build_info_stored(ignored):462 def build_info_stored(ignored):
427 if send_notification:463 self._notify_if_appropriate(send_notification)
428 self.notify()
429 d = self.buildqueue_record.builder.cleanSlave()464 d = self.buildqueue_record.builder.cleanSlave()
430 return d.addCallback(465 return d.addCallback(self._destroy_buildqueue_record)
431 lambda x: self.buildqueue_record.destroySelf())
432466
433 d = self.storeBuildInfo(self, librarian, slave_status)467 d = self.storeBuildInfo(self, librarian, slave_status)
434 return d.addCallback(build_info_stored)468 return d.addCallback(build_info_stored)
@@ -441,16 +475,16 @@
441 MANUALDEPWAIT, store available information, remove BuildQueue475 MANUALDEPWAIT, store available information, remove BuildQueue
442 entry and release builder slave for another job.476 entry and release builder slave for another job.
443 """477 """
444 self.status = BuildStatus.MANUALDEPWAIT478 with DatabaseTransactionPolicy(read_only=False):
479 self.status = BuildStatus.MANUALDEPWAIT
480 transaction.commit()
445481
446 def build_info_stored(ignored):482 def build_info_stored(ignored):
447 logger.critical("***** %s is MANUALDEPWAIT *****"483 logger.critical("***** %s is MANUALDEPWAIT *****"
448 % self.buildqueue_record.builder.name)484 % self.buildqueue_record.builder.name)
449 if send_notification:485 self._notify_if_appropriate(send_notification)
450 self.notify()
451 d = self.buildqueue_record.builder.cleanSlave()486 d = self.buildqueue_record.builder.cleanSlave()
452 return d.addCallback(487 return d.addCallback(self._destroy_buildqueue_record)
453 lambda x: self.buildqueue_record.destroySelf())
454488
455 d = self.storeBuildInfo(self, librarian, slave_status)489 d = self.storeBuildInfo(self, librarian, slave_status)
456 return d.addCallback(build_info_stored)490 return d.addCallback(build_info_stored)
@@ -463,20 +497,29 @@
463 job as CHROOTFAIL, store available information, remove BuildQueue497 job as CHROOTFAIL, store available information, remove BuildQueue
464 and release the builder.498 and release the builder.
465 """499 """
466 self.status = BuildStatus.CHROOTWAIT500 with DatabaseTransactionPolicy(read_only=False):
501 self.status = BuildStatus.CHROOTWAIT
502 transaction.commit()
467503
468 def build_info_stored(ignored):504 def build_info_stored(ignored):
469 logger.critical("***** %s is CHROOTWAIT *****" %505 logger.critical(
470 self.buildqueue_record.builder.name)506 "***** %s is CHROOTWAIT *****",
471 if send_notification:507 self.buildqueue_record.builder.name)
472 self.notify()508
509 self._notify_if_appropriate(send_notification)
473 d = self.buildqueue_record.builder.cleanSlave()510 d = self.buildqueue_record.builder.cleanSlave()
474 return d.addCallback(511 return d.addCallback(self._destroy_buildqueue_record)
475 lambda x: self.buildqueue_record.destroySelf())
476512
477 d = self.storeBuildInfo(self, librarian, slave_status)513 d = self.storeBuildInfo(self, librarian, slave_status)
478 return d.addCallback(build_info_stored)514 return d.addCallback(build_info_stored)
479515
516 def _reset_buildqueue_record(self, ignored_arg=None):
517 """Reset the `BuildQueue` record, in a write transaction."""
518 transaction.commit()
519 with DatabaseTransactionPolicy(read_only=False):
520 self.buildqueue_record.reset()
521 transaction.commit()
522
480 def _handleStatus_BUILDERFAIL(self, librarian, slave_status, logger,523 def _handleStatus_BUILDERFAIL(self, librarian, slave_status, logger,
481 send_notification):524 send_notification):
482 """Handle builder failures.525 """Handle builder failures.
@@ -490,11 +533,8 @@
490 self.buildqueue_record.builder.failBuilder(533 self.buildqueue_record.builder.failBuilder(
491 "Builder returned BUILDERFAIL when asked for its status")534 "Builder returned BUILDERFAIL when asked for its status")
492535
493 def build_info_stored(ignored):
494 # simply reset job
495 self.buildqueue_record.reset()
496 d = self.storeBuildInfo(self, librarian, slave_status)536 d = self.storeBuildInfo(self, librarian, slave_status)
497 return d.addCallback(build_info_stored)537 return d.addCallback(self._reset_buildqueue_record)
498538
499 def _handleStatus_GIVENBACK(self, librarian, slave_status, logger,539 def _handleStatus_GIVENBACK(self, librarian, slave_status, logger,
500 send_notification):540 send_notification):
@@ -514,7 +554,7 @@
514 # the next Paris Summit, infinity has some ideas about how554 # the next Paris Summit, infinity has some ideas about how
515 # to use this content. For now we just ensure it's stored.555 # to use this content. For now we just ensure it's stored.
516 d = self.buildqueue_record.builder.cleanSlave()556 d = self.buildqueue_record.builder.cleanSlave()
517 self.buildqueue_record.reset()557 self._reset_buildqueue_record()
518 return d558 return d
519559
520 d = self.storeBuildInfo(self, librarian, slave_status)560 d = self.storeBuildInfo(self, librarian, slave_status)
521561
=== added file 'lib/lp/buildmaster/testing.py'
--- lib/lp/buildmaster/testing.py 1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/testing.py 2012-01-03 12:26:30 +0000
@@ -0,0 +1,59 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Testing helpers for buildmaster code."""
5
6__metaclass__ = type
7__all__ = [
8 "BuilddManagerTestFixture",
9 ]
10
11from contextlib import contextmanager
12
13import fixtures
14import transaction
15
16from lp.services.database.transaction_policy import DatabaseTransactionPolicy
17
18
19class BuilddManagerTestFixture(fixtures.Fixture):
20 """Helps provide an environment more like `BuilddManager` provides.
21
22 This mimics the default transaction access policy of `BuilddManager`,
23 though it can be configured with a different policy by passing in `store`
24 and/or `read_only`. See `BuilddManager.enterReadOnlyDatabasePolicy`.
25
26 Because this will shift into a read-only database transaction access mode,
27 individual tests that need to do more setup can use the `extraSetUp()`
28 context manager to temporarily shift back to a read-write mode.
29 """
30
31 def __init__(self, store=None, read_only=True):
32 super(BuilddManagerTestFixture, self).__init__()
33 self.policy = DatabaseTransactionPolicy(
34 store=store, read_only=read_only)
35
36 def setUp(self):
37 # Commit everything done so far then shift into a read-only
38 # transaction access mode by default.
39 super(BuilddManagerTestFixture, self).setUp()
40 transaction.commit()
41 self.policy.__enter__()
42 self.addCleanup(self.policy.__exit__, None, None, None)
43
44 @staticmethod
45 @contextmanager
46 def extraSetUp():
47 """Temporarily enter a read-write transaction to do extra setup.
48
49 For example:
50
51 with self.extraSetUp():
52 removeSecurityProxy(self.build).date_finished = None
53
54 On exit it will commit the changes and restore the previous
55 transaction access mode.
56 """
57 with DatabaseTransactionPolicy(read_only=False):
58 yield
59 transaction.commit()
060
=== modified file 'lib/lp/buildmaster/tests/test_builder.py'
--- lib/lp/buildmaster/tests/test_builder.py 2011-12-30 06:14:56 +0000
+++ lib/lp/buildmaster/tests/test_builder.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test Builder features."""4"""Test Builder features."""
@@ -15,6 +15,7 @@
15 AsynchronousDeferredRunTestForBrokenTwisted,15 AsynchronousDeferredRunTestForBrokenTwisted,
16 SynchronousDeferredRunTest,16 SynchronousDeferredRunTest,
17 )17 )
18import transaction
18from twisted.internet.defer import (19from twisted.internet.defer import (
19 CancelledError,20 CancelledError,
20 DeferredList,21 DeferredList,
@@ -60,8 +61,10 @@
60 TrivialBehavior,61 TrivialBehavior,
61 WaitingSlave,62 WaitingSlave,
62 )63 )
64from lp.registry.interfaces.pocket import PackagePublishingPocket
63from lp.services.config import config65from lp.services.config import config
64from lp.services.database.sqlbase import flush_database_updates66from lp.services.database.sqlbase import flush_database_updates
67from lp.services.database.transaction_policy import DatabaseTransactionPolicy
65from lp.services.job.interfaces.job import JobStatus68from lp.services.job.interfaces.job import JobStatus
66from lp.services.log.logger import BufferLogger69from lp.services.log.logger import BufferLogger
67from lp.services.webapp.interfaces import (70from lp.services.webapp.interfaces import (
@@ -152,7 +155,7 @@
152 d = lostbuilding_builder.updateStatus(BufferLogger())155 d = lostbuilding_builder.updateStatus(BufferLogger())
153 def check_slave_status(failure):156 def check_slave_status(failure):
154 self.assertIn('abort', slave.call_log)157 self.assertIn('abort', slave.call_log)
155 # 'Fault' comes from the LostBuildingBrokenSlave, this is158 # 'Fault' comes from the LostBuildingBrokenSlave. This is
156 # just testing that the value is passed through.159 # just testing that the value is passed through.
157 self.assertIsInstance(failure.value, xmlrpclib.Fault)160 self.assertIsInstance(failure.value, xmlrpclib.Fault)
158 return d.addBoth(check_slave_status)161 return d.addBoth(check_slave_status)
@@ -531,6 +534,26 @@
531 # And the old_candidate is superseded:534 # And the old_candidate is superseded:
532 self.assertEqual(BuildStatus.SUPERSEDED, build.status)535 self.assertEqual(BuildStatus.SUPERSEDED, build.status)
533536
537 def test_findBuildCandidate_postprocesses_in_read_write_policy(self):
538 # _findBuildCandidate invokes BuildFarmJob.postprocessCandidate,
539 # which may modify the database. This happens in a read-write
540 # transaction even if _findBuildCandidate itself runs in a
541 # read-only transaction policy.
542
543 # PackageBuildJob.postprocessCandidate will attempt to delete
544 # security builds.
545 pub = self.publisher.getPubSource(
546 sourcename="gedit", status=PackagePublishingStatus.PUBLISHED,
547 archive=self.factory.makeArchive(),
548 pocket=PackagePublishingPocket.SECURITY)
549 pub.createMissingBuilds()
550 transaction.commit()
551 with DatabaseTransactionPolicy(read_only=True):
552 removeSecurityProxy(self.frog_builder)._findBuildCandidate()
553 # The test is that this passes without a "transaction is
554 # read-only" error.
555 transaction.commit()
556
534 def test_acquireBuildCandidate_marks_building(self):557 def test_acquireBuildCandidate_marks_building(self):
535 # acquireBuildCandidate() should call _findBuildCandidate and558 # acquireBuildCandidate() should call _findBuildCandidate and
536 # mark the build as building.559 # mark the build as building.
537560
=== modified file 'lib/lp/buildmaster/tests/test_manager.py'
--- lib/lp/buildmaster/tests/test_manager.py 2012-01-01 02:58:52 +0000
+++ lib/lp/buildmaster/tests/test_manager.py 2012-01-03 12:26:30 +0000
@@ -1,8 +1,9 @@
1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the renovated slave scanner aka BuilddManager."""4"""Tests for the renovated slave scanner aka BuilddManager."""
55
6from collections import namedtuple
6import os7import os
7import signal8import signal
8import time9import time
@@ -34,15 +35,18 @@
34 SlaveScanner,35 SlaveScanner,
35 )36 )
36from lp.buildmaster.model.builder import Builder37from lp.buildmaster.model.builder import Builder
38from lp.buildmaster.model.packagebuild import PackageBuild
37from lp.buildmaster.tests.harness import BuilddManagerTestSetup39from lp.buildmaster.tests.harness import BuilddManagerTestSetup
38from lp.buildmaster.tests.mock_slaves import (40from lp.buildmaster.tests.mock_slaves import (
39 BrokenSlave,41 BrokenSlave,
40 BuildingSlave,42 BuildingSlave,
41 make_publisher,43 make_publisher,
42 OkSlave,44 OkSlave,
45 WaitingSlave,
43 )46 )
44from lp.registry.interfaces.distribution import IDistributionSet47from lp.registry.interfaces.distribution import IDistributionSet
45from lp.services.config import config48from lp.services.config import config
49from lp.services.database.transaction_policy import DatabaseTransactionPolicy
46from lp.services.log.logger import BufferLogger50from lp.services.log.logger import BufferLogger
47from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet51from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
48from lp.testing import (52from lp.testing import (
@@ -58,7 +62,10 @@
58 LaunchpadZopelessLayer,62 LaunchpadZopelessLayer,
59 ZopelessDatabaseLayer,63 ZopelessDatabaseLayer,
60 )64 )
61from lp.testing.sampledata import BOB_THE_BUILDER_NAME65from lp.testing.sampledata import (
66 BOB_THE_BUILDER_NAME,
67 FROG_THE_BUILDER_NAME,
68 )
6269
6370
64class TestSlaveScannerScan(TestCase):71class TestSlaveScannerScan(TestCase):
@@ -76,6 +83,8 @@
76 'bob' builder.83 'bob' builder.
77 """84 """
78 super(TestSlaveScannerScan, self).setUp()85 super(TestSlaveScannerScan, self).setUp()
86 self.read_only = DatabaseTransactionPolicy(read_only=True)
87
79 # Creating the required chroots needed for dispatching.88 # Creating the required chroots needed for dispatching.
80 test_publisher = make_publisher()89 test_publisher = make_publisher()
81 ubuntu = getUtility(IDistributionSet).getByName('ubuntu')90 ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
@@ -83,6 +92,15 @@
83 test_publisher.setUpDefaultDistroSeries(hoary)92 test_publisher.setUpDefaultDistroSeries(hoary)
84 test_publisher.addFakeChroots()93 test_publisher.addFakeChroots()
8594
95 def _enterReadOnly(self):
96 """Go into read-only transaction policy."""
97 self.read_only.__enter__()
98 self.addCleanup(self._exitReadOnly)
99
100 def _exitReadOnly(self):
101 """Leave read-only transaction policy."""
102 self.read_only.__exit__(None, None, None)
103
86 def _resetBuilder(self, builder):104 def _resetBuilder(self, builder):
87 """Reset the given builder and its job."""105 """Reset the given builder and its job."""
88106
@@ -93,6 +111,23 @@
93111
94 transaction.commit()112 transaction.commit()
95113
114 def getFreshBuilder(self, slave=None, name=BOB_THE_BUILDER_NAME,
115 failure_count=0):
116 """Return a builder.
117
118 The builder is taken from sample data, but reset to a usable state.
119 Be careful: this is not a proper factory method. Identical calls
120 return (and reset) the same builder. Don't rely on that though;
121 maybe someday we'll have a proper factory here.
122 """
123 if slave is None:
124 slave = OkSlave()
125 builder = getUtility(IBuilderSet)[name]
126 self._resetBuilder(builder)
127 builder.setSlaveForTesting(slave)
128 builder.failure_count = failure_count
129 return builder
130
96 def assertBuildingJob(self, job, builder, logtail=None):131 def assertBuildingJob(self, job, builder, logtail=None):
97 """Assert the given job is building on the given builder."""132 """Assert the given job is building on the given builder."""
98 from lp.services.job.interfaces.job import JobStatus133 from lp.services.job.interfaces.job import JobStatus
@@ -107,14 +142,14 @@
107 self.assertEqual(build.status, BuildStatus.BUILDING)142 self.assertEqual(build.status, BuildStatus.BUILDING)
108 self.assertEqual(job.logtail, logtail)143 self.assertEqual(job.logtail, logtail)
109144
110 def _getScanner(self, builder_name=None):145 def _getScanner(self, builder_name=None, clock=None):
111 """Instantiate a SlaveScanner object.146 """Instantiate a SlaveScanner object.
112147
113 Replace its default logging handler by a testing version.148 Replace its default logging handler by a testing version.
114 """149 """
115 if builder_name is None:150 if builder_name is None:
116 builder_name = BOB_THE_BUILDER_NAME151 builder_name = BOB_THE_BUILDER_NAME
117 scanner = SlaveScanner(builder_name, BufferLogger())152 scanner = SlaveScanner(builder_name, BufferLogger(), clock=clock)
118 scanner.logger.name = 'slave-scanner'153 scanner.logger.name = 'slave-scanner'
119154
120 return scanner155 return scanner
@@ -130,17 +165,15 @@
130 def testScanDispatchForResetBuilder(self):165 def testScanDispatchForResetBuilder(self):
131 # A job gets dispatched to the sampledata builder after it's reset.166 # A job gets dispatched to the sampledata builder after it's reset.
132167
133 # Reset sampledata builder.168 # Obtain a builder. Initialize failure count to 1 so that
134 builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME]169 # _checkDispatch can make sure that a successful dispatch resets
135 self._resetBuilder(builder)170 # the count to 0.
136 builder.setSlaveForTesting(OkSlave())171 builder = self.getFreshBuilder(failure_count=1)
137 # Set this to 1 here so that _checkDispatch can make sure it's
138 # reset to 0 after a successful dispatch.
139 builder.failure_count = 1
140172
141 # Run 'scan' and check its result.173 # Run 'scan' and check its result.
142 self.layer.txn.commit()174 self.layer.txn.commit()
143 self.layer.switchDbUser(config.builddmaster.dbuser)175 self.layer.switchDbUser(config.builddmaster.dbuser)
176 self._enterReadOnly()
144 scanner = self._getScanner()177 scanner = self._getScanner()
145 d = defer.maybeDeferred(scanner.scan)178 d = defer.maybeDeferred(scanner.scan)
146 d.addCallback(self._checkDispatch, builder)179 d.addCallback(self._checkDispatch, builder)
@@ -153,20 +186,18 @@
153 to the asynchonous dispatcher and the builder remained active186 to the asynchonous dispatcher and the builder remained active
154 and IDLE.187 and IDLE.
155 """188 """
156 self.assertTrue(slave is None, "Unexpected slave.")189 self.assertIs(None, slave, "Unexpected slave.")
157190
158 builder = getUtility(IBuilderSet).get(builder.id)191 builder = getUtility(IBuilderSet).get(builder.id)
159 self.assertTrue(builder.builderok)192 self.assertTrue(builder.builderok)
160 self.assertTrue(builder.currentjob is None)193 self.assertIs(None, builder.currentjob)
161194
162 def testNoDispatchForMissingChroots(self):195 def testNoDispatchForMissingChroots(self):
163 # When a required chroot is not present the `scan` method196 # When a required chroot is not present the `scan` method
164 # should not return any `RecordingSlaves` to be processed197 # should not return any `RecordingSlaves` to be processed
165 # and the builder used should remain active and IDLE.198 # and the builder used should remain active and IDLE.
166199
167 # Reset sampledata builder.200 builder = self.getFreshBuilder()
168 builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME]
169 self._resetBuilder(builder)
170201
171 # Remove hoary/i386 chroot.202 # Remove hoary/i386 chroot.
172 login('foo.bar@canonical.com')203 login('foo.bar@canonical.com')
@@ -179,6 +210,7 @@
179210
180 # Run 'scan' and check its result.211 # Run 'scan' and check its result.
181 self.layer.switchDbUser(config.builddmaster.dbuser)212 self.layer.switchDbUser(config.builddmaster.dbuser)
213 self._enterReadOnly()
182 scanner = self._getScanner()214 scanner = self._getScanner()
183 d = defer.maybeDeferred(scanner.singleCycle)215 d = defer.maybeDeferred(scanner.singleCycle)
184 d.addCallback(self._checkNoDispatch, builder)216 d.addCallback(self._checkNoDispatch, builder)
@@ -220,6 +252,7 @@
220252
221 # Run 'scan' and check its result.253 # Run 'scan' and check its result.
222 self.layer.switchDbUser(config.builddmaster.dbuser)254 self.layer.switchDbUser(config.builddmaster.dbuser)
255 self._enterReadOnly()
223 scanner = self._getScanner()256 scanner = self._getScanner()
224 d = defer.maybeDeferred(scanner.scan)257 d = defer.maybeDeferred(scanner.scan)
225 d.addCallback(self._checkJobRescued, builder, job)258 d.addCallback(self._checkJobRescued, builder, job)
@@ -255,25 +288,27 @@
255288
256 # Run 'scan' and check its result.289 # Run 'scan' and check its result.
257 self.layer.switchDbUser(config.builddmaster.dbuser)290 self.layer.switchDbUser(config.builddmaster.dbuser)
291 self._enterReadOnly()
258 scanner = self._getScanner()292 scanner = self._getScanner()
259 d = defer.maybeDeferred(scanner.scan)293 d = defer.maybeDeferred(scanner.scan)
260 d.addCallback(self._checkJobUpdated, builder, job)294 d.addCallback(self._checkJobUpdated, builder, job)
261 return d295 return d
262296
263 def test_scan_with_nothing_to_dispatch(self):297 def test_scan_with_nothing_to_dispatch(self):
264 factory = LaunchpadObjectFactory()298 builder = self.factory.makeBuilder()
265 builder = factory.makeBuilder()
266 builder.setSlaveForTesting(OkSlave())299 builder.setSlaveForTesting(OkSlave())
300 transaction.commit()
301 self._enterReadOnly()
267 scanner = self._getScanner(builder_name=builder.name)302 scanner = self._getScanner(builder_name=builder.name)
268 d = scanner.scan()303 d = scanner.scan()
269 return d.addCallback(self._checkNoDispatch, builder)304 return d.addCallback(self._checkNoDispatch, builder)
270305
271 def test_scan_with_manual_builder(self):306 def test_scan_with_manual_builder(self):
272 # Reset sampledata builder.307 # Reset sampledata builder.
273 builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME]308 builder = self.getFreshBuilder()
274 self._resetBuilder(builder)
275 builder.setSlaveForTesting(OkSlave())
276 builder.manual = True309 builder.manual = True
310 transaction.commit()
311 self._enterReadOnly()
277 scanner = self._getScanner()312 scanner = self._getScanner()
278 d = scanner.scan()313 d = scanner.scan()
279 d.addCallback(self._checkNoDispatch, builder)314 d.addCallback(self._checkNoDispatch, builder)
@@ -281,10 +316,10 @@
281316
282 def test_scan_with_not_ok_builder(self):317 def test_scan_with_not_ok_builder(self):
283 # Reset sampledata builder.318 # Reset sampledata builder.
284 builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME]319 builder = self.getFreshBuilder()
285 self._resetBuilder(builder)
286 builder.setSlaveForTesting(OkSlave())
287 builder.builderok = False320 builder.builderok = False
321 transaction.commit()
322 self._enterReadOnly()
288 scanner = self._getScanner()323 scanner = self._getScanner()
289 d = scanner.scan()324 d = scanner.scan()
290 # Because the builder is not ok, we can't use _checkNoDispatch.325 # Because the builder is not ok, we can't use _checkNoDispatch.
@@ -293,25 +328,27 @@
293 return d328 return d
294329
295 def test_scan_of_broken_slave(self):330 def test_scan_of_broken_slave(self):
296 builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME]331 builder = self.getFreshBuilder(slave=BrokenSlave())
297 self._resetBuilder(builder)332 transaction.commit()
298 builder.setSlaveForTesting(BrokenSlave())333 self._enterReadOnly()
299 builder.failure_count = 0
300 scanner = self._getScanner(builder_name=builder.name)334 scanner = self._getScanner(builder_name=builder.name)
301 d = scanner.scan()335 d = scanner.scan()
302 return assert_fails_with(d, xmlrpclib.Fault)336 return assert_fails_with(d, xmlrpclib.Fault)
303337
304 def _assertFailureCounting(self, builder_count, job_count,338 def _assertFailureCounting(self, builder_count, job_count,
305 expected_builder_count, expected_job_count):339 expected_builder_count, expected_job_count):
340 # Avoid circular imports.
341 from lp.buildmaster import manager as manager_module
342
306 # If scan() fails with an exception, failure_counts should be343 # If scan() fails with an exception, failure_counts should be
307 # incremented. What we do with the results of the failure344 # incremented. What we do with the results of the failure
308 # counts is tested below separately, this test just makes sure that345 # counts is tested below separately, this test just makes sure that
309 # scan() is setting the counts.346 # scan() is setting the counts.
310 def failing_scan():347 def failing_scan():
311 return defer.fail(Exception("fake exception"))348 return defer.fail(Exception("fake exception"))
349
312 scanner = self._getScanner()350 scanner = self._getScanner()
313 scanner.scan = failing_scan351 scanner.scan = failing_scan
314 from lp.buildmaster import manager as manager_module
315 self.patch(manager_module, 'assessFailureCounts', FakeMethod())352 self.patch(manager_module, 'assessFailureCounts', FakeMethod())
316 builder = getUtility(IBuilderSet)[scanner.builder_name]353 builder = getUtility(IBuilderSet)[scanner.builder_name]
317354
@@ -459,6 +496,60 @@
459 d.addCallback(check_cancelled, builder, buildqueue)496 d.addCallback(check_cancelled, builder, buildqueue)
460 return d497 return d
461498
499 def makeFakeFailure(self):
500 """Produce a fake failure for use with SlaveScanner._scanFailed."""
501 FakeFailure = namedtuple('FakeFailure', ['getErrorMessage', 'check'])
502 return FakeFailure(
503 FakeMethod(self.factory.getUniqueString()),
504 FakeMethod(True))
505
506 def test_interleaved_success_and_failure_do_not_interfere(self):
507 # It's possible for one builder to fail while another continues
508 # to function properly. When that happens, the failed builder
509 # may cause database changes to be rolled back. But that does
510 # not affect the functioning builder.
511 clock = task.Clock()
512
513 broken_builder = self.getFreshBuilder(
514 slave=BrokenSlave(), name=BOB_THE_BUILDER_NAME)
515 broken_scanner = self._getScanner(builder_name=broken_builder.name)
516 good_builder = self.getFreshBuilder(
517 slave=WaitingSlave(), name=FROG_THE_BUILDER_NAME)
518 good_build = self.factory.makeBinaryPackageBuild(
519 distroarchseries=self.factory.makeDistroArchSeries())
520
521 # The good build is being handled by the good builder.
522 buildqueue = good_build.queueBuild()
523 buildqueue.builder = good_builder
524
525 removeSecurityProxy(good_build.build_farm_job).date_started = UTC_NOW
526
527 # The good builder requests information from a successful build,
528 # and up receiving it, updates the build's metadata.
529 # Our dependencies string goes into the build, and its
530 # date_finished will be set.
531 dependencies = self.factory.getUniqueString()
532 PackageBuild.storeBuildInfo(
533 good_build, None, {'dependencies': dependencies})
534 clock.advance(1)
535
536 # The broken scanner experiences a failure before the good
537 # scanner is receiving its data. This aborts the ongoing
538 # transaction.
539 # As a somewhat weird example, if the builder changed its own
540 # title, that change will be rolled back.
541 original_broken_builder_title = broken_builder.title
542 broken_builder.title = self.factory.getUniqueString()
543 broken_scanner._scanFailed(self.makeFakeFailure())
544
545 # The work done by the good scanner is retained. The
546 # storeBuildInfo code committed it.
547 self.assertEqual(dependencies, good_build.dependencies)
548 self.assertIsNot(None, good_build.date_finished)
549
550 # The work done by the broken scanner is rolled back.
551 self.assertEqual(original_broken_builder_title, broken_builder.title)
552
462553
463class TestCancellationChecking(TestCaseWithFactory):554class TestCancellationChecking(TestCaseWithFactory):
464 """Unit tests for the checkCancellation method."""555 """Unit tests for the checkCancellation method."""
465556
=== modified file 'lib/lp/buildmaster/tests/test_packagebuild.py'
--- lib/lp/buildmaster/tests/test_packagebuild.py 2011-12-30 06:14:56 +0000
+++ lib/lp/buildmaster/tests/test_packagebuild.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for `IPackageBuild`."""4"""Tests for `IPackageBuild`."""
@@ -12,6 +12,7 @@
12import tempfile12import tempfile
1313
14from storm.store import Store14from storm.store import Store
15from testtools.deferredruntest import AsynchronousDeferredRunTest
15from zope.component import getUtility16from zope.component import getUtility
16from zope.security.interfaces import Unauthorized17from zope.security.interfaces import Unauthorized
17from zope.security.proxy import removeSecurityProxy18from zope.security.proxy import removeSecurityProxy
@@ -26,8 +27,10 @@
26 IPackageBuildSet,27 IPackageBuildSet,
27 IPackageBuildSource,28 IPackageBuildSource,
28 )29 )
30from lp.buildmaster.model.builder import BuilderSlave
29from lp.buildmaster.model.buildfarmjob import BuildFarmJob31from lp.buildmaster.model.buildfarmjob import BuildFarmJob
30from lp.buildmaster.model.packagebuild import PackageBuild32from lp.buildmaster.model.packagebuild import PackageBuild
33from lp.buildmaster.testing import BuilddManagerTestFixture
31from lp.buildmaster.tests.mock_slaves import WaitingSlave34from lp.buildmaster.tests.mock_slaves import WaitingSlave
32from lp.registry.interfaces.pocket import PackagePublishingPocket35from lp.registry.interfaces.pocket import PackagePublishingPocket
33from lp.services.config import config36from lp.services.config import config
@@ -281,12 +284,10 @@
281284
282285
283class TestHandleStatusMixin:286class TestHandleStatusMixin:
284 """Tests for `IPackageBuild`s handleStatus method.287 """Tests for `IPackageBuild`s handleStatus method."""
285
286 This should be run with a Trial TestCase.
287 """
288288
289 layer = LaunchpadZopelessLayer289 layer = LaunchpadZopelessLayer
290 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=20)
290291
291 def makeBuild(self):292 def makeBuild(self):
292 """Allow classes to override the build with which the test runs."""293 """Allow classes to override the build with which the test runs."""
@@ -303,7 +304,7 @@
303 self.build.buildqueue_record.setDateStarted(UTC_NOW)304 self.build.buildqueue_record.setDateStarted(UTC_NOW)
304 self.slave = WaitingSlave('BuildStatus.OK')305 self.slave = WaitingSlave('BuildStatus.OK')
305 self.slave.valid_file_hashes.append('test_file_hash')306 self.slave.valid_file_hashes.append('test_file_hash')
306 builder.setSlaveForTesting(self.slave)307 self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(self.slave))
307308
308 # We overwrite the buildmaster root to use a temp directory.309 # We overwrite the buildmaster root to use a temp directory.
309 tempdir = tempfile.mkdtemp()310 tempdir = tempfile.mkdtemp()
@@ -321,6 +322,8 @@
321 removeSecurityProxy(self.build).verifySuccessfulUpload = FakeMethod(322 removeSecurityProxy(self.build).verifySuccessfulUpload = FakeMethod(
322 result=True)323 result=True)
323324
325 self.useFixture(BuilddManagerTestFixture())
326
324 def assertResultCount(self, count, result):327 def assertResultCount(self, count, result):
325 self.assertEquals(328 self.assertEquals(
326 1, len(os.listdir(os.path.join(self.upload_root, result))))329 1, len(os.listdir(os.path.join(self.upload_root, result))))
@@ -344,7 +347,7 @@
344 def got_status(ignored):347 def got_status(ignored):
345 self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.status)348 self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.status)
346 self.assertResultCount(0, "failed")349 self.assertResultCount(0, "failed")
347 self.assertIdentical(None, self.build.buildqueue_record)350 self.assertIs(None, self.build.buildqueue_record)
348351
349 d = self.build.handleStatus('OK', None, {352 d = self.build.handleStatus('OK', None, {
350 'filemap': {'/tmp/myfile.py': 'test_file_hash'},353 'filemap': {'/tmp/myfile.py': 'test_file_hash'},
@@ -365,7 +368,8 @@
365368
366 def test_handleStatus_OK_sets_build_log(self):369 def test_handleStatus_OK_sets_build_log(self):
367 # The build log is set during handleStatus.370 # The build log is set during handleStatus.
368 removeSecurityProxy(self.build).log = None371 with BuilddManagerTestFixture.extraSetUp():
372 removeSecurityProxy(self.build).log = None
369 self.assertEqual(None, self.build.log)373 self.assertEqual(None, self.build.log)
370 d = self.build.handleStatus('OK', None, {374 d = self.build.handleStatus('OK', None, {
371 'filemap': {'myfile.py': 'test_file_hash'},375 'filemap': {'myfile.py': 'test_file_hash'},
@@ -386,14 +390,10 @@
386390
387 def got_status(ignored):391 def got_status(ignored):
388 if expected_notification:392 if expected_notification:
389 self.failIf(393 self.assertNotEqual(
390 len(pop_notifications()) == 0,394 0, len(pop_notifications()), "No notifications received.")
391 "No notifications received")
392 else:395 else:
393 self.failIf(396 self.assertContentEqual([], pop_notifications())
394 len(pop_notifications()) > 0,
395 "Notifications received")
396
397 d = self.build.handleStatus(status, None, {})397 d = self.build.handleStatus(status, None, {})
398 return d.addCallback(got_status)398 return d.addCallback(got_status)
399399
@@ -408,7 +408,9 @@
408408
409 def test_date_finished_set(self):409 def test_date_finished_set(self):
410 # The date finished is updated during handleStatus_OK.410 # The date finished is updated during handleStatus_OK.
411 removeSecurityProxy(self.build).date_finished = None411 with BuilddManagerTestFixture.extraSetUp():
412 removeSecurityProxy(self.build).date_finished = None
413
412 self.assertEqual(None, self.build.date_finished)414 self.assertEqual(None, self.build.date_finished)
413 d = self.build.handleStatus('OK', None, {415 d = self.build.handleStatus('OK', None, {
414 'filemap': {'myfile.py': 'test_file_hash'},416 'filemap': {'myfile.py': 'test_file_hash'},
415417
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2012-01-01 02:58:52 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2012-01-03 12:26:30 +0000
@@ -13,14 +13,15 @@
1313
14from pytz import utc14from pytz import utc
15from storm.locals import Store15from storm.locals import Store
16from testtools.deferredruntest import AsynchronousDeferredRunTest
16import transaction17import transaction
17from twisted.trial.unittest import TestCase as TrialTestCase
18from zope.component import getUtility18from zope.component import getUtility
19from zope.security.proxy import removeSecurityProxy19from zope.security.proxy import removeSecurityProxy
2020
21from lp.app.errors import NotFoundError21from lp.app.errors import NotFoundError
22from lp.buildmaster.enums import BuildStatus22from lp.buildmaster.enums import BuildStatus
23from lp.buildmaster.interfaces.buildqueue import IBuildQueue23from lp.buildmaster.interfaces.buildqueue import IBuildQueue
24from lp.buildmaster.model.builder import BuilderSlave
24from lp.buildmaster.model.buildfarmjob import BuildFarmJob25from lp.buildmaster.model.buildfarmjob import BuildFarmJob
25from lp.buildmaster.model.packagebuild import PackageBuild26from lp.buildmaster.model.packagebuild import PackageBuild
26from lp.buildmaster.tests.mock_slaves import WaitingSlave27from lp.buildmaster.tests.mock_slaves import WaitingSlave
@@ -588,14 +589,11 @@
588 self.assertEquals(0, len(notifications))589 self.assertEquals(0, len(notifications))
589590
590591
591class TestBuildNotifications(TrialTestCase):592class TestBuildNotifications(TestCaseWithFactory):
592593
593 layer = LaunchpadZopelessLayer594 layer = LaunchpadZopelessLayer
594595
595 def setUp(self):596 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=20)
596 super(TestBuildNotifications, self).setUp()
597 from lp.testing.factory import LaunchpadObjectFactory
598 self.factory = LaunchpadObjectFactory()
599597
600 def prepare_build(self, fake_successful_upload=False):598 def prepare_build(self, fake_successful_upload=False):
601 queue_record = self.factory.makeSourcePackageRecipeBuildJob()599 queue_record = self.factory.makeSourcePackageRecipeBuildJob()
@@ -608,7 +606,7 @@
608 result=True)606 result=True)
609 queue_record.builder = self.factory.makeBuilder()607 queue_record.builder = self.factory.makeBuilder()
610 slave = WaitingSlave('BuildStatus.OK')608 slave = WaitingSlave('BuildStatus.OK')
611 queue_record.builder.setSlaveForTesting(slave)609 self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(slave))
612 return build610 return build
613611
614 def assertDeferredNotifyCount(self, status, build, expected_count):612 def assertDeferredNotifyCount(self, status, build, expected_count):
@@ -666,5 +664,5 @@
666664
667665
668class TestHandleStatusForSPRBuild(666class TestHandleStatusForSPRBuild(
669 MakeSPRecipeBuildMixin, TestHandleStatusMixin, TrialTestCase):667 MakeSPRecipeBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
670 """IPackageBuild.handleStatus works with SPRecipe builds."""668 """IPackageBuild.handleStatus works with SPRecipe builds."""
671669
=== modified file 'lib/lp/services/database/tests/test_transaction_policy.py'
--- lib/lp/services/database/tests/test_transaction_policy.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/database/tests/test_transaction_policy.py 2012-01-03 12:26:30 +0000
@@ -76,16 +76,26 @@
7676
77 self.assertRaises(InternalError, make_forbidden_update)77 self.assertRaises(InternalError, make_forbidden_update)
7878
79 def test_will_not_start_in_ongoing_transaction(self):79 def test_will_not_go_read_only_when_read_write_transaction_ongoing(self):
80 # You cannot enter a DatabaseTransactionPolicy while already in80 # You cannot enter a read-only DatabaseTransactionPolicy while in an
81 # a transaction.81 # active read-write transaction.
82 def enter_policy():82 def enter_policy():
83 with DatabaseTransactionPolicy():83 with DatabaseTransactionPolicy(read_only=True):
84 pass84 pass
8585
86 self.writeToDatabase()86 self.writeToDatabase()
87 self.assertRaises(TransactionInProgress, enter_policy)87 self.assertRaises(TransactionInProgress, enter_policy)
8888
89 def test_will_go_read_write_when_read_write_transaction_ongoing(self):
90 # You can enter a read-write DatabaseTransactionPolicy while in an
91 # active read-write transaction.
92 def enter_policy():
93 with DatabaseTransactionPolicy(read_only=False):
94 pass
95
96 self.writeToDatabase()
97 enter_policy() # No exception.
98
89 def test_successful_exit_requires_commit_or_abort(self):99 def test_successful_exit_requires_commit_or_abort(self):
90 # If a read-write policy exits normally (which would probably100 # If a read-write policy exits normally (which would probably
91 # indicate successful completion of its code), it requires that101 # indicate successful completion of its code), it requires that
92102
=== modified file 'lib/lp/services/database/transaction_policy.py'
--- lib/lp/services/database/transaction_policy.py 2011-12-30 06:14:56 +0000
+++ lib/lp/services/database/transaction_policy.py 2012-01-03 12:26:30 +0000
@@ -92,9 +92,18 @@
9292
93 :raise TransactionInProgress: if a transaction was already ongoing.93 :raise TransactionInProgress: if a transaction was already ongoing.
94 """94 """
95 self._checkNoTransaction(95 # We must check the transaction status before checking the current
96 "Entered DatabaseTransactionPolicy while in a transaction.")96 # policy because getting the policy causes a status change.
97 in_transaction = self._isInTransaction()
97 self.previous_policy = self._getCurrentPolicy()98 self.previous_policy = self._getCurrentPolicy()
99 # If the current transaction is read-write and we're moving to
100 # read-only then we should check for a transaction. If the current
101 # policy is read-only then we don't care if a transaction is in
102 # progress.
103 if in_transaction and self.read_only and not self.previous_policy:
104 raise TransactionInProgress(
105 "Attempting to enter a read-only transaction while holding "
106 "open a read-write transaction.")
98 self._setPolicy(self.read_only)107 self._setPolicy(self.read_only)
99 # Commit should include the policy itself. If this breaks108 # Commit should include the policy itself. If this breaks
100 # because the transaction was already in a failed state before109 # because the transaction was already in a failed state before
@@ -133,8 +142,11 @@
133 def _isInTransaction(self):142 def _isInTransaction(self):
134 """Is our store currently in a transaction?"""143 """Is our store currently in a transaction?"""
135 pg_connection = self.store._connection._raw_connection144 pg_connection = self.store._connection._raw_connection
136 status = pg_connection.get_transaction_status()145 if pg_connection is None:
137 return status != TRANSACTION_STATUS_IDLE146 return False
147 else:
148 status = pg_connection.get_transaction_status()
149 return status != TRANSACTION_STATUS_IDLE
138150
139 def _checkNoTransaction(self, error_msg):151 def _checkNoTransaction(self, error_msg):
140 """Verify that no transaction is ongoing.152 """Verify that no transaction is ongoing.
141153
=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py'
--- lib/lp/soyuz/tests/test_binarypackagebuild.py 2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test Build features."""4"""Test Build features."""
@@ -10,7 +10,6 @@
1010
11import pytz11import pytz
12from storm.store import Store12from storm.store import Store
13from twisted.trial.unittest import TestCase as TrialTestCase
14from zope.component import getUtility13from zope.component import getUtility
15from zope.security.proxy import removeSecurityProxy14from zope.security.proxy import removeSecurityProxy
1615
@@ -18,6 +17,7 @@
18from lp.buildmaster.interfaces.builder import IBuilderSet17from lp.buildmaster.interfaces.builder import IBuilderSet
19from lp.buildmaster.interfaces.buildqueue import IBuildQueue18from lp.buildmaster.interfaces.buildqueue import IBuildQueue
20from lp.buildmaster.interfaces.packagebuild import IPackageBuild19from lp.buildmaster.interfaces.packagebuild import IPackageBuild
20from lp.buildmaster.model.builder import BuilderSlave
21from lp.buildmaster.model.buildqueue import BuildQueue21from lp.buildmaster.model.buildqueue import BuildQueue
22from lp.buildmaster.tests.mock_slaves import WaitingSlave22from lp.buildmaster.tests.mock_slaves import WaitingSlave
23from lp.buildmaster.tests.test_packagebuild import (23from lp.buildmaster.tests.test_packagebuild import (
@@ -48,6 +48,7 @@
48 logout,48 logout,
49 TestCaseWithFactory,49 TestCaseWithFactory,
50 )50 )
51from lp.testing.fakemethod import FakeMethod
51from lp.testing.layers import (52from lp.testing.layers import (
52 DatabaseFunctionalLayer,53 DatabaseFunctionalLayer,
53 LaunchpadZopelessLayer,54 LaunchpadZopelessLayer,
@@ -522,7 +523,9 @@
522 self.build = gedit_src_hist.createMissingBuilds()[0]523 self.build = gedit_src_hist.createMissingBuilds()[0]
523524
524 self.builder = self.factory.makeBuilder()525 self.builder = self.factory.makeBuilder()
525 self.builder.setSlaveForTesting(WaitingSlave('BuildStatus.OK'))526 self.patch(
527 BuilderSlave, 'makeBuilderSlave',
528 FakeMethod(WaitingSlave('BuildStatus.OK')))
526 self.build.buildqueue_record.markAsBuilding(self.builder)529 self.build.buildqueue_record.markAsBuilding(self.builder)
527530
528 def testDependencies(self):531 def testDependencies(self):
@@ -568,7 +571,7 @@
568571
569572
570class TestHandleStatusForBinaryPackageBuild(573class TestHandleStatusForBinaryPackageBuild(
571 MakeBinaryPackageBuildMixin, TestHandleStatusMixin, TrialTestCase):574 MakeBinaryPackageBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
572 """IPackageBuild.handleStatus works with binary builds."""575 """IPackageBuild.handleStatus works with binary builds."""
573576
574577
575578
=== modified file 'lib/lp/translations/model/translationtemplatesbuildbehavior.py'
--- lib/lp/translations/model/translationtemplatesbuildbehavior.py 2012-01-01 02:58:52 +0000
+++ lib/lp/translations/model/translationtemplatesbuildbehavior.py 2012-01-03 12:26:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""An `IBuildFarmJobBehavior` for `TranslationTemplatesBuildJob`.4"""An `IBuildFarmJobBehavior` for `TranslationTemplatesBuildJob`.
@@ -16,6 +16,7 @@
16import tempfile16import tempfile
1717
18import pytz18import pytz
19import transaction
19from twisted.internet import defer20from twisted.internet import defer
20from zope.component import getUtility21from zope.component import getUtility
21from zope.interface import implements22from zope.interface import implements
@@ -28,6 +29,7 @@
28 )29 )
29from lp.buildmaster.model.buildfarmjobbehavior import BuildFarmJobBehaviorBase30from lp.buildmaster.model.buildfarmjobbehavior import BuildFarmJobBehaviorBase
30from lp.registry.interfaces.productseries import IProductSeriesSet31from lp.registry.interfaces.productseries import IProductSeriesSet
32from lp.services.database.transaction_policy import DatabaseTransactionPolicy
31from lp.translations.interfaces.translationimportqueue import (33from lp.translations.interfaces.translationimportqueue import (
32 ITranslationImportQueue,34 ITranslationImportQueue,
33 )35 )
@@ -132,13 +134,16 @@
132 def storeBuildInfo(build, queue_item, build_status):134 def storeBuildInfo(build, queue_item, build_status):
133 """See `IPackageBuild`."""135 """See `IPackageBuild`."""
134 def got_log(lfa_id):136 def got_log(lfa_id):
135 build.build.log = lfa_id137 transaction.commit()
136 build.build.builder = queue_item.builder138 with DatabaseTransactionPolicy(read_only=False):
137 build.build.date_started = queue_item.date_started139 build.build.log = lfa_id
138 # XXX cprov 20060615 bug=120584: Currently buildduration includes140 build.build.builder = queue_item.builder
139 # the scanner latency, it should really be asking the slave for141 build.build.date_started = queue_item.date_started
140 # the duration spent building locally.142 # XXX cprov 20060615 bug=120584: Currently buildduration
141 build.build.date_finished = datetime.datetime.now(pytz.UTC)143 # includes the scanner latency. It should really be
144 # asking the slave for the duration spent building locally.
145 build.build.date_finished = datetime.datetime.now(pytz.UTC)
146 transaction.commit()
142147
143 d = build.getLogFromSlave(build, queue_item)148 d = build.getLogFromSlave(build, queue_item)
144 return d.addCallback(got_log)149 return d.addCallback(got_log)