Merge lp:~gmb/launchpad/create-blobjobs-bug-519205 into lp:launchpad/db-devel

Proposed by Graham Binns
Status: Merged
Approved by: Eleanor Berger
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~gmb/launchpad/create-blobjobs-bug-519205
Merge into: lp:launchpad/db-devel
Diff against target: 285 lines (+164/-9)
6 files modified
lib/canonical/launchpad/browser/temporaryblobstorage.py (+14/-4)
lib/lp/bugs/configure.zcml (+12/-0)
lib/lp/bugs/interfaces/apportjob.py (+5/-0)
lib/lp/bugs/model/apportjob.py (+28/-4)
lib/lp/bugs/tests/test_apportjob.py (+104/-1)
lib/lp/bugs/utilities/filebugdataparser.py (+1/-0)
To merge this branch: bzr merge lp:~gmb/launchpad/create-blobjobs-bug-519205
Reviewer Review Type Date Requested Status
Eleanor Berger (community) code Approve
Review via email: mp+19088@code.launchpad.net

Commit message

TemporaryBlobStorageAddView will now create a new ProcessApportBlobJob for each uploaded BLOB.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

This branch is the next stage of moving the work for processing Apport
BLOBs out of the +filebug view and into the Jobs system.

The changes in this branch mean that when a BLOB is uploaded Launchpad
will automatically create a new ProcessApportBlobJob for it. This job
can then be polled later by +filebug to see if the BLOB has been parsed
properly.

== lib/canonical/launchpad/browser/temporaryblobstorage.py ==

 - I've updated TemporaryBlobStorageAddView so that it now creates a new
   ProcessApportBlobJob for the uploaded BLOB. I've moved the uploading
   functionality into a separate method so that it can be tested
   directly rather than having to test it via the 'Continue' action.

== lib/lp/bugs/configure.zcml ==

 - I've added ZCML for the ApportBlobJob interfaces.

== lib/lp/bugs/interfaces/apportjob.py ==

 - I've added a getByBlobUUID() method to IApportBlobJobSource so that
   we can look up Jobs by BLOB UUID. This will be used in a subsequent
   branch to poll for processing jobs being complete.

== lib/lp/bugs/model/apportjob.py ==

 - I've updated ProcessApportBlobJob.create() so that it will not create
   a new job for BLOB if there's already a job, complete or otherwise,
   in the database.
 - I've added an implementation of getByBlobUUID() to
   ProcessApportBlobJob.

== lib/lp/bugs/tests/test_apportjob.py ==

 - I've added tests for the changes to ProcessApportBlobJob and
   TemporaryBlobStorageAddView.

== lib/lp/bugs/utilities/filebugdataparser.py ==

 - I've cleaned up some lint by adding FileBugData to the __all__ clause
   of the module.

No lint.

Revision history for this message
Eleanor Berger (intellectronica) wrote :

(12:00:36) intellectronica: gmb: i suspect the 'blob=322819' in aaron's XXX in lib/lp/bugs/model/apportjob.py is a the result of a search&replace that went out of control?
(12:00:49) intellectronica: it's not in the current diff, but if you agree you can change it back now
(12:01:03) gmb: intellectronica: Ah, wacky fun. Yes, that'll be an over-eager %s.
(12:01:06) gmb: I'll fix it.
(12:03:14) intellectronica: gmb: in getByBlobUUID, i'd change the way you do the assert. i'd do `if jobs_for_blob.count() > 1: raise AssertionError(...)`
(12:03:47) intellectronica: which both rids you from the strange '< 2' condition, and from the assert statement that is always a bit confusing
(12:03:51) intellectronica: what do you think?
(12:04:11) gmb: intellectronica: I agree. I'll change it.
(12:07:15) intellectronica: gmb: other than that it all looks great. r=me

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

Incremental diff of fixage:

=== modified file 'lib/lp/bugs/model/apportjob.py'
--- lib/lp/bugs/model/apportjob.py 2010-02-11 11:01:12 +0000
+++ lib/lp/bugs/model/apportjob.py 2010-02-11 12:08:02 +0000
@@ -83,7 +83,7 @@
         self.job = Job()
         self.blob = blob
         self.job_type = job_type
- # XXX AaronBentley 2009-01-29 blob=322819: This should be a
+ # XXX AaronBentley 2009-01-29 bug=322819: This should be a
         # bytestring, but the DB representation is unicode.
         self._json_data = json_data.decode('utf-8')

@@ -196,8 +196,9 @@
             raise SQLObjectNotFound(
                 "No ProcessApportBlobJob found for UUID %s" % uuid)

- assert jobs_for_blob.count() < 2, (
- "There can only be one ProcessApportBlobJob for a BLOB.")
+ if jobs_for_blob.count() > 1:
+ raise AssertionError(
+ "There can only be one ProcessApportBlobJob for a BLOB.")

         return cls(jobs_for_blob.any())

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/browser/temporaryblobstorage.py'
2--- lib/canonical/launchpad/browser/temporaryblobstorage.py 2009-09-18 13:24:29 +0000
3+++ lib/canonical/launchpad/browser/temporaryblobstorage.py 2010-02-11 12:16:27 +0000
4@@ -16,6 +16,8 @@
5 BlobTooLarge, ITemporaryBlobStorage, ITemporaryStorageManager)
6 from canonical.librarian.interfaces import UploadFailed
7
8+from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource
9+
10
11 class TemporaryBlobStorageAddView(LaunchpadFormView):
12 # XXX: gary 2009-09-18 bug=31358
13@@ -40,13 +42,21 @@
14 # being named like that.
15 @action('Continue', name='FORM_SUBMIT')
16 def continue_action(self, action, data):
17+ uuid = self.store_blob(data['blob'])
18+ self.request.response.setHeader('X-Launchpad-Blob-Token', uuid)
19+ self.request.response.addInfoNotification(
20+ 'Your ticket is "%s"' % uuid)
21+
22+ def store_blob(self, blob):
23+ """Store a blob and return its UUID."""
24 try:
25- uuid = getUtility(ITemporaryStorageManager).new(data['blob'])
26- self.request.response.setHeader('X-Launchpad-Blob-Token', uuid)
27- self.request.response.addInfoNotification(
28- 'Your ticket is "%s"' % uuid)
29+ uuid = getUtility(ITemporaryStorageManager).new(blob)
30 except BlobTooLarge:
31 self.addError('Uploaded file was too large.')
32 except UploadFailed:
33 self.addError('File storage unavailable - try again later.')
34
35+ # Create ProcessApportBlobJob for the BLOB.
36+ blob = getUtility(ITemporaryStorageManager).fetch(uuid)
37+ getUtility(IProcessApportBlobJobSource).create(blob)
38+ return uuid
39
40=== modified file 'lib/lp/bugs/configure.zcml'
41--- lib/lp/bugs/configure.zcml 2010-02-09 16:44:17 +0000
42+++ lib/lp/bugs/configure.zcml 2010-02-11 12:16:27 +0000
43@@ -914,4 +914,16 @@
44 <allow
45 interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource"/>
46 </securedutility>
47+
48+ <!-- ProcessApportBlobJobs -->
49+ <class class="lp.bugs.model.apportjob.ProcessApportBlobJob">
50+ <allow interface="lp.bugs.interfaces.apportjob.IApportJob" />
51+ <allow interface="lp.bugs.interfaces.apportjob.IProcessApportBlobJob"/>
52+ </class>
53+ <securedutility
54+ component="lp.bugs.model.apportjob.ProcessApportBlobJob"
55+ provides="lp.bugs.interfaces.apportjob.IProcessApportBlobJobSource">
56+ <allow
57+ interface="lp.bugs.interfaces.apportjob.IProcessApportBlobJobSource"/>
58+ </securedutility>
59 </configure>
60
61=== modified file 'lib/lp/bugs/interfaces/apportjob.py'
62--- lib/lp/bugs/interfaces/apportjob.py 2010-02-01 15:49:37 +0000
63+++ lib/lp/bugs/interfaces/apportjob.py 2010-02-11 12:16:26 +0000
64@@ -7,6 +7,8 @@
65 'ApportJobType',
66 'IApportJob',
67 'IApportJobSource',
68+ 'IProcessApportBlobJob',
69+ 'IProcessApportBlobJobSource',
70 ]
71
72 from zope.interface import Attribute, Interface
73@@ -57,6 +59,9 @@
74 def create(bug):
75 """Create a new IApportJob for a bug."""
76
77+ def getByBlobUUID(uuid):
78+ """For a given BLOB UUID, return any jobs pertaining to that BLOB."""
79+
80
81 class IProcessApportBlobJob(IRunnableJob):
82 """A Job to process an Apport BLOB."""
83
84=== modified file 'lib/lp/bugs/model/apportjob.py'
85--- lib/lp/bugs/model/apportjob.py 2010-02-03 22:20:14 +0000
86+++ lib/lp/bugs/model/apportjob.py 2010-02-11 12:16:26 +0000
87@@ -83,8 +83,8 @@
88 self.job = Job()
89 self.blob = blob
90 self.job_type = job_type
91- # XXX AaronBentley 2009-01-29 blob=322819: This should be a bytestring,
92- # but the DB representation is unicode.
93+ # XXX AaronBentley 2009-01-29 bug=322819: This should be a
94+ # bytestring, but the DB representation is unicode.
95 self._json_data = json_data.decode('utf-8')
96
97 @classmethod
98@@ -163,14 +163,16 @@
99 @classmethod
100 def create(cls, blob):
101 """See `IProcessApportBlobJobSource`."""
102- # If there's already a job for the bug, don't create a new one.
103+ # If there's already a job for the BLOB, don't create a new one.
104+ # We also include jobs which have been completed when checking
105+ # for exisiting jobs, since a BLOB should only be processed
106+ # once.
107 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
108 job_for_blob = store.find(
109 ApportJob,
110 ApportJob.blob == blob,
111 ApportJob.job_type == cls.class_job_type,
112 ApportJob.job == Job.id,
113- Job.id.is_in(Job.ready_jobs)
114 ).any()
115
116 if job_for_blob is not None:
117@@ -178,6 +180,28 @@
118 else:
119 return super(ProcessApportBlobJob, cls).create(blob)
120
121+ @classmethod
122+ def getByBlobUUID(cls, uuid):
123+ """See `IApportJobSource`."""
124+ store = IStore(ApportJob)
125+ blob = store.get(TemporaryBlobStorage, uuid == uuid)
126+ jobs_for_blob = store.find(
127+ ApportJob,
128+ ApportJob.blob == blob,
129+ ApportJob.job_type == cls.class_job_type,
130+ ApportJob.job == Job.id
131+ )
132+
133+ if jobs_for_blob.is_empty():
134+ raise SQLObjectNotFound(
135+ "No ProcessApportBlobJob found for UUID %s" % uuid)
136+
137+ if jobs_for_blob.count() > 1:
138+ raise AssertionError(
139+ "There can only be one ProcessApportBlobJob for a BLOB.")
140+
141+ return cls(jobs_for_blob.any())
142+
143 def run(self):
144 """See `IRunnableJob`."""
145 self.blob.file_alias.open()
146
147=== modified file 'lib/lp/bugs/tests/test_apportjob.py'
148--- lib/lp/bugs/tests/test_apportjob.py 2010-02-03 22:20:14 +0000
149+++ lib/lp/bugs/tests/test_apportjob.py 2010-02-11 12:16:27 +0000
150@@ -13,13 +13,18 @@
151
152 from canonical.config import config
153 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
154-from canonical.testing import LaunchpadZopelessLayer
155+from canonical.launchpad.interfaces.temporaryblobstorage import (
156+ ITemporaryStorageManager)
157+from canonical.launchpad.webapp.interfaces import ILaunchpadRoot
158+from canonical.testing import (
159+ LaunchpadFunctionalLayer, LaunchpadZopelessLayer)
160
161 from lp.bugs.interfaces.apportjob import ApportJobType
162 from lp.bugs.model.apportjob import (
163 ApportJob, ApportJobDerived, ProcessApportBlobJob)
164 from lp.bugs.utilities.filebugdataparser import FileBugDataParser
165 from lp.testing import TestCaseWithFactory
166+from lp.testing.views import create_initialized_view
167
168
169 class ApportJobTestCase(TestCaseWithFactory):
170@@ -157,6 +162,104 @@
171 "LibrarianFileAlias %s" % (
172 attachment['filename'], file_alias.id))
173
174+ def test_getByBlobUUID(self):
175+ # ProcessApportBlobJob.getByBlobUUID takes a BLOB UUID as a
176+ # parameter and returns any jobs for that BLOB.
177+ uuid = self.blob.uuid
178+
179+ job = ProcessApportBlobJob.create(self.blob)
180+ job_from_uuid = ProcessApportBlobJob.getByBlobUUID(uuid)
181+ self.assertEqual(
182+ job, job_from_uuid,
183+ "Job returend by getByBlobUUID() did not match original job.")
184+ self.assertEqual(
185+ self.blob, job_from_uuid.blob,
186+ "BLOB referenced by Job returned by getByBlobUUID() did not "
187+ "match original BLOB.")
188+
189+ def test_create_job_creates_only_one(self):
190+ # ProcessApportBlobJob.create() will create only one
191+ # ProcessApportBlobJob for a given BLOB, no matter how many
192+ # times it is called.
193+ current_jobs = list(ProcessApportBlobJob.iterReady())
194+ self.assertEqual(
195+ 0, len(current_jobs),
196+ "There should be no ProcessApportBlobJobs. Found %s" %
197+ len(current_jobs))
198+
199+ job = ProcessApportBlobJob.create(self.blob)
200+ current_jobs = list(ProcessApportBlobJob.iterReady())
201+ self.assertEqual(
202+ 1, len(current_jobs),
203+ "There should be only one ProcessApportBlobJob. Found %s" %
204+ len(current_jobs))
205+
206+ another_job = ProcessApportBlobJob.create(self.blob)
207+ current_jobs = list(ProcessApportBlobJob.iterReady())
208+ self.assertEqual(
209+ 1, len(current_jobs),
210+ "There should be only one ProcessApportBlobJob. Found %s" %
211+ len(current_jobs))
212+
213+ # If the job is complete, it will no longer show up in the list
214+ # of ready jobs. However, it won't be possible to create a new
215+ # job to process the BLOB because each BLOB can only have one
216+ # ProcessApportBlobJob.
217+ job.job.start()
218+ job.job.complete()
219+ current_jobs = list(ProcessApportBlobJob.iterReady())
220+ self.assertEqual(
221+ 0, len(current_jobs),
222+ "There should be no ready ProcessApportBlobJobs. Found %s" %
223+ len(current_jobs))
224+
225+ yet_another_job = ProcessApportBlobJob.create(self.blob)
226+ current_jobs = list(ProcessApportBlobJob.iterReady())
227+ self.assertEqual(
228+ 0, len(current_jobs),
229+ "There should be no new ProcessApportBlobJobs. Found %s" %
230+ len(current_jobs))
231+
232+ # In fact, yet_another_job will be the same job as before, since
233+ # it's attached to the same BLOB.
234+ self.assertEqual(job.id, yet_another_job.id, "Jobs do not match.")
235+
236+
237+class TestTemporaryBlobStorageAddView(TestCaseWithFactory):
238+ """Test case for the TemporaryBlobStorageAddView."""
239+
240+ layer = LaunchpadFunctionalLayer
241+
242+ def setUp(self):
243+ super(TestTemporaryBlobStorageAddView, self).setUp()
244+
245+ # Create a BLOB using existing testing data.
246+ testfiles = os.path.join(config.root, 'lib/lp/bugs/tests/testfiles')
247+ blob_file = open(
248+ os.path.join(testfiles, 'extra_filebug_data.msg'))
249+ self.blob_data = blob_file.read()
250+ blob_file.close()
251+
252+ def test_adding_blob_adds_job(self):
253+ # Using the TemporaryBlobStorageAddView to upload a new BLOB
254+ # will add a new ProcessApportBlobJob for that BLOB.
255+ view = create_initialized_view(
256+ getUtility(ILaunchpadRoot), '+storeblob')
257+
258+ # The view's store_blob method stores the blob in the database
259+ # and returns its UUID.
260+ blob_uuid = view.store_blob(self.blob_data)
261+ transaction.commit()
262+
263+ # A new ProcessApportBlobJob will have been created for the
264+ # BLOB.
265+ blob = getUtility(ITemporaryStorageManager).fetch(blob_uuid)
266+ job = ProcessApportBlobJob.getByBlobUUID(blob_uuid)
267+
268+ self.assertEqual(
269+ blob, job.blob,
270+ "BLOB attached to Job returned by getByBlobUUID() did not match "
271+ "expected BLOB.")
272
273
274 def test_suite():
275
276=== modified file 'lib/lp/bugs/utilities/filebugdataparser.py'
277--- lib/lp/bugs/utilities/filebugdataparser.py 2010-02-03 22:20:14 +0000
278+++ lib/lp/bugs/utilities/filebugdataparser.py 2010-02-11 12:16:27 +0000
279@@ -4,6 +4,7 @@
280
281 __metaclass__ = type
282 __all__ = [
283+ 'FileBugData',
284 'FileBugDataParser',
285 ]
286

Subscribers

People subscribed via source and target branches

to status/vote changes: