Merge lp:~wallyworld/launchpad/recipe-build-removed-recipe into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Leonard Richardson
Approved revision: no longer in the source branch.
Merged at revision: 12304
Proposed branch: lp:~wallyworld/launchpad/recipe-build-removed-recipe
Merge into: lp:launchpad
Diff against target: 151 lines (+80/-7)
4 files modified
lib/lp/archiveuploader/tests/test_uploadprocessor.py (+34/-0)
lib/lp/archiveuploader/uploadprocessor.py (+26/-7)
lib/lp/code/model/sourcepackagerecipebuild.py (+4/-0)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+16/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/recipe-build-removed-recipe
Reviewer Review Type Date Requested Status
Leonard Richardson (community) Approve
Review via email: mp+48103@code.launchpad.net

Commit message

[r=leonardr][ui=none][bug=698032] When a recipe build has been removed since a recipe build was started, ensure the archive uploader does not fall over.

Description of the change

See bug 698032. When a recipe build has been removed since a recipe build was started the archive uploader will fall over.

== Implementation ==

Add a check to the upload processor to see if a recipe has been deleted. Not quite as simple as that, since there's a potential race condition whereby any check placed at the beginning of the process could pass and the recipe could then be deleted during the processing (the effects will depend on the transaction boundaries etc). So, the implementation is to check at the beginning and again at the end, deferring any error processing till the last check. The error processing is to log a warning. Any post build clean up is done as per a build failure to ensure any files are not left lying around.

A check was also added to the mail notification method of SourcePackageRecipeBuild to stop any recipe deletions causing that part to error.

== Tests ==

Addded a new test to lp.archiveloader.tests.test_uploadprocessor:
- testSourcePackageRecipeBuild_deleted_recipe()

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/archiveuploader/uploadprocessor.py
  lib/lp/archiveuploader/tests/test_uploadprocessor.py
  lib/lp/code/model/sourcepackagerecipebuild.py

./lib/lp/archiveuploader/tests/test_uploadprocessor.py
    1263: E301 expected 1 blank line, found 2

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

To clarify the implementation:

We want to stop the oops when upload processing is attempted for a build with a deleted recipe. We don't want to email any error message but rather we just want to log a warning. We also need to ensure any normal post upload cleanup is done (as per for the standard upload failure case).

So the approach was to check at the start of the process() method to see if the recipe was deleted and flag it, don't attempt the upload processing, and defer the handling of the problem to occur along with other failure mode processing (so that all error handling is done in the one place).

The comments about the transaction boundaries are probably not relevant - it appears the upload processing is wrapped inside a transaction so there's no likelihood of any race condition. But the implementation would handle this case too.

Revision history for this message
Leonard Richardson (leonardr) wrote :

This looks fine.

As you say, the code should work even if it's run in multiple transactions and the recipe is deleted during processing. But, maybe you want to test this--add a test that nothing happens when you call notify() on a build whose recipe has been deleted. It shouldn't be difficult.

review: Approve
Revision history for this message
Ian Booth (wallyworld) wrote :

I've added a test as requested in
lp.code.model.tests.test_sourcepackagerecipebuild

On 02/02/11 01:51, Leonard Richardson wrote:
> Review: Approve
> This looks fine.
>
> As you say, the code should work even if it's run in multiple transactions and the recipe is deleted during processing. But, maybe you want to test this--add a test that nothing happens when you call notify() on a build whose recipe has been deleted. It shouldn't be difficult.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2011-01-27 17:09:28 +0000
+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2011-02-02 05:29:53 +0000
@@ -2040,6 +2040,40 @@
2040 body = mail.get_payload(decode=True)2040 body = mail.get_payload(decode=True)
2041 self.assertIn('Upload Log: http', body)2041 self.assertIn('Upload Log: http', body)
20422042
2043 def doDeletedRecipeBuild(self):
2044 # A source package recipe build will fail if the recipe is deleted.
2045
2046 # Upload a source package
2047 archive = self.factory.makeArchive()
2048 archive.require_virtualized = False
2049 build = self.factory.makeSourcePackageRecipeBuild(sourcename=u"bar",
2050 distroseries=self.breezy, archive=archive)
2051 bq = self.factory.makeSourcePackageRecipeBuildJob(recipe_build=build)
2052 # Commit so the build cookie has the right ids.
2053 Store.of(build).flush()
2054 leaf_name = build.getUploadDirLeaf(build.getBuildCookie())
2055 os.mkdir(os.path.join(self.incoming_folder, leaf_name))
2056 self.options.context = 'buildd'
2057 self.options.builds = True
2058 build.jobStarted()
2059 build.recipe.destroySelf()
2060 # Commit so date_started is recorded and doesn't cause constraint
2061 # violations later.
2062 Store.of(build).flush()
2063 build.status = BuildStatus.UPLOADING
2064 build.date_finished = UTC_NOW
2065 BuildUploadHandler(self.uploadprocessor, self.incoming_folder,
2066 leaf_name).process()
2067 self.layer.txn.commit()
2068 return build
2069
2070 def testSourcePackageRecipeBuild_deleted_recipe(self):
2071 build = self.doDeletedRecipeBuild()
2072 self.assertEquals(BuildStatus.FAILEDTOUPLOAD, build.status)
2073 self.assertEquals(None, build.builder)
2074 self.assertIsNot(None, build.duration)
2075 self.assertIs(None, build.upload_log)
2076
2043 def testBuildWithInvalidStatus(self):2077 def testBuildWithInvalidStatus(self):
2044 # Builds with an invalid (non-UPLOADING) status should trigger2078 # Builds with an invalid (non-UPLOADING) status should trigger
2045 # a warning but be left alone.2079 # a warning but be left alone.
20462080
=== modified file 'lib/lp/archiveuploader/uploadprocessor.py'
--- lib/lp/archiveuploader/uploadprocessor.py 2011-01-26 21:49:54 +0000
+++ lib/lp/archiveuploader/uploadprocessor.py 2011-02-02 05:29:53 +0000
@@ -74,6 +74,9 @@
74 BuildStatus,74 BuildStatus,
75 )75 )
76from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSet76from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSet
77from lp.code.interfaces.sourcepackagerecipebuild import (
78 ISourcePackageRecipeBuild,
79 )
77from lp.registry.interfaces.distribution import IDistributionSet80from lp.registry.interfaces.distribution import IDistributionSet
78from lp.registry.interfaces.person import IPersonSet81from lp.registry.interfaces.person import IPersonSet
79from lp.services.log.logger import BufferLogger82from lp.services.log.logger import BufferLogger
@@ -643,11 +646,20 @@
643 "Expected build status to be 'UPLOADING', was %s. Ignoring." %646 "Expected build status to be 'UPLOADING', was %s. Ignoring." %
644 self.build.status.name)647 self.build.status.name)
645 return648 return
646 self.processor.log.debug("Build %s found" % self.build.id)
647 try:649 try:
648 [changes_file] = self.locateChangesFiles()650 # The recipe may have been deleted so we need to flag that here
649 logger.debug("Considering changefile %s" % changes_file)651 # and will handle below. We check so that we don't go to the
650 result = self.processChangesFile(changes_file, logger)652 # expense of doing an unnecessary upload. We don't just exit here
653 # because we want the standard cleanup to occur.
654 recipe_deleted = (ISourcePackageRecipeBuild.providedBy(self.build)
655 and self.build.recipe is None)
656 if recipe_deleted:
657 result = UploadStatusEnum.FAILED
658 else:
659 self.processor.log.debug("Build %s found" % self.build.id)
660 [changes_file] = self.locateChangesFiles()
661 logger.debug("Considering changefile %s" % changes_file)
662 result = self.processChangesFile(changes_file, logger)
651 except (KeyboardInterrupt, SystemExit):663 except (KeyboardInterrupt, SystemExit):
652 raise664 raise
653 except:665 except:
@@ -664,9 +676,16 @@
664 not self.build.verifySuccessfulUpload()):676 not self.build.verifySuccessfulUpload()):
665 self.build.status = BuildStatus.FAILEDTOUPLOAD677 self.build.status = BuildStatus.FAILEDTOUPLOAD
666 if self.build.status != BuildStatus.FULLYBUILT:678 if self.build.status != BuildStatus.FULLYBUILT:
667 self.build.storeUploadLog(logger.getLogBuffer())679 if recipe_deleted:
668 self.build.notify(extra_info="Uploading build %s failed." %680 # For a deleted recipe, no need to notify that uploading has
669 self.upload)681 # failed - we just log a warning.
682 self.processor.log.warn(
683 "Recipe for build %s was deleted. Ignoring." %
684 self.upload)
685 else:
686 self.build.storeUploadLog(logger.getLogBuffer())
687 self.build.notify(extra_info="Uploading build %s failed." %
688 self.upload)
670 else:689 else:
671 self.build.notify()690 self.build.notify()
672 self.processor.ztm.commit()691 self.processor.ztm.commit()
673692
=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py 2011-01-28 05:56:59 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py 2011-02-02 05:29:53 +0000
@@ -292,6 +292,9 @@
292292
293 def notify(self, extra_info=None):293 def notify(self, extra_info=None):
294 """See `IPackageBuild`."""294 """See `IPackageBuild`."""
295 # If our recipe has been deleted, any notification will fail.
296 if self.recipe is None:
297 return
295 mailer = SourcePackageRecipeBuildMailer.forStatus(self)298 mailer = SourcePackageRecipeBuildMailer.forStatus(self)
296 mailer.sendAll()299 mailer.sendAll()
297300
@@ -334,6 +337,7 @@
334 """See `IPackageBuild`."""337 """See `IPackageBuild`."""
335 d = super(SourcePackageRecipeBuild, self)._handleStatus_OK(338 d = super(SourcePackageRecipeBuild, self)._handleStatus_OK(
336 librarian, slave_status, logger)339 librarian, slave_status, logger)
340
337 def uploaded_build(ignored):341 def uploaded_build(ignored):
338 # Base implementation doesn't notify on success.342 # Base implementation doesn't notify on success.
339 if self.status == BuildStatus.FULLYBUILT:343 if self.status == BuildStatus.FULLYBUILT:
340344
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2011-01-31 22:52:20 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2011-02-02 05:29:53 +0000
@@ -457,6 +457,22 @@
457 self.assertEqual(457 self.assertEqual(
458 expected.body, message.get_payload(decode=True))458 expected.body, message.get_payload(decode=True))
459459
460 def test_notify_when_recipe_deleted(self):
461 """Notify does nothing if recipe has been deleted."""
462 person = self.factory.makePerson(name='person')
463 cake = self.factory.makeSourcePackageRecipe(
464 name=u'recipe', owner=person)
465 pantry = self.factory.makeArchive(name='ppa')
466 secret = self.factory.makeDistroSeries(name=u'distroseries')
467 build = self.factory.makeSourcePackageRecipeBuild(
468 recipe=cake, distroseries=secret, archive=pantry)
469 removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
470 cake.destroySelf()
471 IStore(build).flush()
472 build.notify()
473 notifications = pop_notifications()
474 self.assertEquals(0, len(notifications))
475
460 def test_handleStatusNotifies(self):476 def test_handleStatusNotifies(self):
461 """"handleStatus causes notification, even if OK."""477 """"handleStatus causes notification, even if OK."""
462478