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
1=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
2--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2011-01-27 17:09:28 +0000
3+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2011-02-02 05:29:53 +0000
4@@ -2040,6 +2040,40 @@
5 body = mail.get_payload(decode=True)
6 self.assertIn('Upload Log: http', body)
7
8+ def doDeletedRecipeBuild(self):
9+ # A source package recipe build will fail if the recipe is deleted.
10+
11+ # Upload a source package
12+ archive = self.factory.makeArchive()
13+ archive.require_virtualized = False
14+ build = self.factory.makeSourcePackageRecipeBuild(sourcename=u"bar",
15+ distroseries=self.breezy, archive=archive)
16+ bq = self.factory.makeSourcePackageRecipeBuildJob(recipe_build=build)
17+ # Commit so the build cookie has the right ids.
18+ Store.of(build).flush()
19+ leaf_name = build.getUploadDirLeaf(build.getBuildCookie())
20+ os.mkdir(os.path.join(self.incoming_folder, leaf_name))
21+ self.options.context = 'buildd'
22+ self.options.builds = True
23+ build.jobStarted()
24+ build.recipe.destroySelf()
25+ # Commit so date_started is recorded and doesn't cause constraint
26+ # violations later.
27+ Store.of(build).flush()
28+ build.status = BuildStatus.UPLOADING
29+ build.date_finished = UTC_NOW
30+ BuildUploadHandler(self.uploadprocessor, self.incoming_folder,
31+ leaf_name).process()
32+ self.layer.txn.commit()
33+ return build
34+
35+ def testSourcePackageRecipeBuild_deleted_recipe(self):
36+ build = self.doDeletedRecipeBuild()
37+ self.assertEquals(BuildStatus.FAILEDTOUPLOAD, build.status)
38+ self.assertEquals(None, build.builder)
39+ self.assertIsNot(None, build.duration)
40+ self.assertIs(None, build.upload_log)
41+
42 def testBuildWithInvalidStatus(self):
43 # Builds with an invalid (non-UPLOADING) status should trigger
44 # a warning but be left alone.
45
46=== modified file 'lib/lp/archiveuploader/uploadprocessor.py'
47--- lib/lp/archiveuploader/uploadprocessor.py 2011-01-26 21:49:54 +0000
48+++ lib/lp/archiveuploader/uploadprocessor.py 2011-02-02 05:29:53 +0000
49@@ -74,6 +74,9 @@
50 BuildStatus,
51 )
52 from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSet
53+from lp.code.interfaces.sourcepackagerecipebuild import (
54+ ISourcePackageRecipeBuild,
55+ )
56 from lp.registry.interfaces.distribution import IDistributionSet
57 from lp.registry.interfaces.person import IPersonSet
58 from lp.services.log.logger import BufferLogger
59@@ -643,11 +646,20 @@
60 "Expected build status to be 'UPLOADING', was %s. Ignoring." %
61 self.build.status.name)
62 return
63- self.processor.log.debug("Build %s found" % self.build.id)
64 try:
65- [changes_file] = self.locateChangesFiles()
66- logger.debug("Considering changefile %s" % changes_file)
67- result = self.processChangesFile(changes_file, logger)
68+ # The recipe may have been deleted so we need to flag that here
69+ # and will handle below. We check so that we don't go to the
70+ # expense of doing an unnecessary upload. We don't just exit here
71+ # because we want the standard cleanup to occur.
72+ recipe_deleted = (ISourcePackageRecipeBuild.providedBy(self.build)
73+ and self.build.recipe is None)
74+ if recipe_deleted:
75+ result = UploadStatusEnum.FAILED
76+ else:
77+ self.processor.log.debug("Build %s found" % self.build.id)
78+ [changes_file] = self.locateChangesFiles()
79+ logger.debug("Considering changefile %s" % changes_file)
80+ result = self.processChangesFile(changes_file, logger)
81 except (KeyboardInterrupt, SystemExit):
82 raise
83 except:
84@@ -664,9 +676,16 @@
85 not self.build.verifySuccessfulUpload()):
86 self.build.status = BuildStatus.FAILEDTOUPLOAD
87 if self.build.status != BuildStatus.FULLYBUILT:
88- self.build.storeUploadLog(logger.getLogBuffer())
89- self.build.notify(extra_info="Uploading build %s failed." %
90- self.upload)
91+ if recipe_deleted:
92+ # For a deleted recipe, no need to notify that uploading has
93+ # failed - we just log a warning.
94+ self.processor.log.warn(
95+ "Recipe for build %s was deleted. Ignoring." %
96+ self.upload)
97+ else:
98+ self.build.storeUploadLog(logger.getLogBuffer())
99+ self.build.notify(extra_info="Uploading build %s failed." %
100+ self.upload)
101 else:
102 self.build.notify()
103 self.processor.ztm.commit()
104
105=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
106--- lib/lp/code/model/sourcepackagerecipebuild.py 2011-01-28 05:56:59 +0000
107+++ lib/lp/code/model/sourcepackagerecipebuild.py 2011-02-02 05:29:53 +0000
108@@ -292,6 +292,9 @@
109
110 def notify(self, extra_info=None):
111 """See `IPackageBuild`."""
112+ # If our recipe has been deleted, any notification will fail.
113+ if self.recipe is None:
114+ return
115 mailer = SourcePackageRecipeBuildMailer.forStatus(self)
116 mailer.sendAll()
117
118@@ -334,6 +337,7 @@
119 """See `IPackageBuild`."""
120 d = super(SourcePackageRecipeBuild, self)._handleStatus_OK(
121 librarian, slave_status, logger)
122+
123 def uploaded_build(ignored):
124 # Base implementation doesn't notify on success.
125 if self.status == BuildStatus.FULLYBUILT:
126
127=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
128--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2011-01-31 22:52:20 +0000
129+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2011-02-02 05:29:53 +0000
130@@ -457,6 +457,22 @@
131 self.assertEqual(
132 expected.body, message.get_payload(decode=True))
133
134+ def test_notify_when_recipe_deleted(self):
135+ """Notify does nothing if recipe has been deleted."""
136+ person = self.factory.makePerson(name='person')
137+ cake = self.factory.makeSourcePackageRecipe(
138+ name=u'recipe', owner=person)
139+ pantry = self.factory.makeArchive(name='ppa')
140+ secret = self.factory.makeDistroSeries(name=u'distroseries')
141+ build = self.factory.makeSourcePackageRecipeBuild(
142+ recipe=cake, distroseries=secret, archive=pantry)
143+ removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
144+ cake.destroySelf()
145+ IStore(build).flush()
146+ build.notify()
147+ notifications = pop_notifications()
148+ self.assertEquals(0, len(notifications))
149+
150 def test_handleStatusNotifies(self):
151 """"handleStatus causes notification, even if OK."""
152