Merge lp:~henninge/launchpad/bug-461818 into lp:launchpad
- bug-461818
- Merge into devel
Proposed by
Henning Eggers
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Henning Eggers | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~henninge/launchpad/bug-461818 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
431 lines 5 files modified
lib/lp/registry/model/product.py (+2/-2) lib/lp/services/database/configure.zcml (+1/-1) lib/lp/services/database/prejoin.py (+9/-11) lib/lp/services/database/tests/test_prejoin.py (+33/-26) lib/lp/translations/model/potemplate.py (+66/-63) |
||||
To merge this branch: | bzr merge lp:~henninge/launchpad/bug-461818 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | code | Approve | |
Review via email: mp+14085@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote : | # |
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : | # |
Good branch overall, with some much-needed cleanup. A few changes discussed on IRC:
* just_for_count is ugly; replace with do_prefetch and use the existing ordering parameter if necessary.
* A comment needed work.
* It'd be nice, though not absolutely necessary at this point, to have the SQL conditions rewritten in Storm.
Jeroen
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/registry/model/product.py' |
2 | --- lib/lp/registry/model/product.py 2009-10-16 15:00:55 +0000 |
3 | +++ lib/lp/registry/model/product.py 2009-10-28 16:28:12 +0000 |
4 | @@ -63,7 +63,7 @@ |
5 | from lp.registry.model.productlicense import ProductLicense |
6 | from lp.registry.model.productrelease import ProductRelease |
7 | from lp.registry.model.productseries import ProductSeries |
8 | -from lp.services.database.precache import precache |
9 | +from lp.services.database.prejoin import prejoin |
10 | from lp.answers.model.question import ( |
11 | QuestionTargetSearch, QuestionTargetMixin) |
12 | from lp.blueprints.model.specification import ( |
13 | @@ -1293,7 +1293,7 @@ |
14 | |
15 | # We only want Product - the other tables are just to populate |
16 | # the cache. |
17 | - return precache(results) |
18 | + return prejoin(results) |
19 | |
20 | def featuredTranslatables(self, maximumproducts=8): |
21 | """See `IProductSet`""" |
22 | |
23 | === modified file 'lib/lp/services/database/configure.zcml' |
24 | --- lib/lp/services/database/configure.zcml 2009-07-17 02:25:09 +0000 |
25 | +++ lib/lp/services/database/configure.zcml 2009-10-28 16:28:12 +0000 |
26 | @@ -8,7 +8,7 @@ |
27 | xmlns:i18n="http://namespaces.zope.org/i18n" |
28 | i18n_domain="launchpad"> |
29 | |
30 | - <class class="lp.services.database.precache.PrecacheResultSet"> |
31 | + <class class="lp.services.database.prejoin.PrejoinResultSet"> |
32 | <allow interface="storm.zope.interfaces.IResultSet" /> |
33 | <allow attributes="__getslice__" /> |
34 | </class> |
35 | |
36 | === renamed file 'lib/lp/services/database/precache.py' => 'lib/lp/services/database/prejoin.py' |
37 | --- lib/lp/services/database/precache.py 2009-10-05 11:33:04 +0000 |
38 | +++ lib/lp/services/database/prejoin.py 2009-10-28 16:28:12 +0000 |
39 | @@ -1,21 +1,17 @@ |
40 | # Copyright 2009 Canonical Ltd. This software is licensed under the |
41 | # GNU Affero General Public License version 3 (see the file LICENSE). |
42 | |
43 | -"""precache for Storm. |
44 | - |
45 | -XXX stub 2009-06-18 bug=388798: This feature may be misnamed, should be |
46 | -moved to a better location, and needs tests. |
47 | -""" |
48 | +"""Prejoin for Storm.""" |
49 | |
50 | __metaclass__ = type |
51 | -__all__ = ['precache'] |
52 | +__all__ = ['prejoin'] |
53 | |
54 | from storm.zope.interfaces import IResultSet |
55 | |
56 | from lazr.delegates import delegates |
57 | |
58 | -class PrecacheResultSet: |
59 | - """Precache support. |
60 | +class PrejoinResultSet: |
61 | + """Prejoin support. |
62 | |
63 | Wrap a ResultSet, trimming unwanted items. The unwanted items |
64 | are still pulled from the database and populate the Storm caches. |
65 | @@ -25,7 +21,7 @@ |
66 | The preferred solution is support in Storm core, so we can just do |
67 | something like: |
68 | |
69 | - >>> results = store.find(Product).precache( |
70 | + >>> results = store.find(Product).prejoin( |
71 | ... (Person, EmailAddress), |
72 | ... Product._ownerID == Person.id, |
73 | ... EmailAddress.personID == Person.id) |
74 | @@ -36,9 +32,11 @@ |
75 | self.return_slice = return_slice |
76 | |
77 | def _chain(self, result_set): |
78 | - return PrecacheResultSet(result_set, self.return_slice) |
79 | + return PrejoinResultSet(result_set, self.return_slice) |
80 | |
81 | def _chomp(self, row): |
82 | + if row is None: |
83 | + return None |
84 | elems = row[self.return_slice] |
85 | if len(elems) == 1: |
86 | return elems[0] |
87 | @@ -113,4 +111,4 @@ |
88 | """See `IResultSet`.""" |
89 | raise NotImplementedError("cached") |
90 | |
91 | -precache = PrecacheResultSet |
92 | +prejoin = PrejoinResultSet |
93 | |
94 | === renamed file 'lib/lp/services/database/tests/test_precache.py' => 'lib/lp/services/database/tests/test_prejoin.py' |
95 | --- lib/lp/services/database/tests/test_precache.py 2009-10-05 11:33:04 +0000 |
96 | +++ lib/lp/services/database/tests/test_prejoin.py 2009-10-28 16:28:12 +0000 |
97 | @@ -1,24 +1,22 @@ |
98 | # Copyright 2009 Canonical Ltd. This software is licensed under the |
99 | # GNU Affero General Public License version 3 (see the file LICENSE). |
100 | |
101 | -"""precache module tests.""" |
102 | +"""prejoin module tests.""" |
103 | |
104 | __metaclass__ = type |
105 | __all__ = [] |
106 | |
107 | import unittest |
108 | |
109 | -from storm.expr import And, Join, LeftJoin |
110 | - |
111 | from canonical.launchpad.interfaces import IMasterStore |
112 | from canonical.testing.layers import LaunchpadZopelessLayer |
113 | from lp.registry.model.person import Person |
114 | from lp.registry.model.product import Product |
115 | -from lp.services.database.precache import precache, PrecacheResultSet |
116 | +from lp.services.database.prejoin import prejoin, PrejoinResultSet |
117 | from lp.testing import TestCase |
118 | |
119 | |
120 | -class PrecacheTestCase(TestCase): |
121 | +class PrejoinTestCase(TestCase): |
122 | layer = LaunchpadZopelessLayer |
123 | |
124 | def setUp(self): |
125 | @@ -27,73 +25,82 @@ |
126 | # All products |
127 | self.standard_result = self.store.find(Product).order_by(Product.name) |
128 | |
129 | - # All products, with owner and preferred email address precached. |
130 | + # All products, with owner and preferred email address prejoined. |
131 | # Note that because some Product owners have multiple email |
132 | - # addresses, this query returns more results. precache needs |
133 | + # addresses, this query returns more results. prejoin needs |
134 | # to hide this from callsites. |
135 | self.unwrapped_result = self.store.find( |
136 | (Product, Person), |
137 | Product._ownerID == Person.id).order_by(Product.name) |
138 | - self.precache_result = precache(self.unwrapped_result) |
139 | + self.prejoin_result = prejoin(self.unwrapped_result) |
140 | |
141 | - def verify(self, precached, normal): |
142 | - # Ensure our precached result really is a PrecacheResultSet. |
143 | + def verify(self, prejoined, normal): |
144 | + # Ensure our prejoined result really is a PrejoinResultSet. |
145 | # One of our methods might not be sticky, and we have ended up |
146 | # with a normal Storm ResultSet. |
147 | self.assertTrue( |
148 | - isinstance(precached, PrecacheResultSet), |
149 | - "Expected a PrecacheResultSet, got %s" % repr(precached)) |
150 | + isinstance(prejoined, PrejoinResultSet), |
151 | + "Expected a PrejoinResultSet, got %s" % repr(prejoined)) |
152 | |
153 | # Confirm the two result sets return identical results. |
154 | - self.assertEqual(list(normal), list(precached)) |
155 | + self.assertEqual(list(normal), list(prejoined)) |
156 | |
157 | def test_count(self): |
158 | self.assertEqual( |
159 | self.standard_result.count(), |
160 | - self.precache_result.count()) |
161 | + self.prejoin_result.count()) |
162 | |
163 | def test_copy(self): |
164 | - copy = self.precache_result.copy() |
165 | + copy = self.prejoin_result.copy() |
166 | self.verify(copy, self.standard_result) |
167 | |
168 | def test_config(self): |
169 | - self.precache_result.config(offset=3, limit=2) |
170 | + self.prejoin_result.config(offset=3, limit=2) |
171 | self.standard_result.config(offset=3, limit=2) |
172 | - self.verify(self.precache_result, self.standard_result) |
173 | + self.verify(self.prejoin_result, self.standard_result) |
174 | |
175 | def test_config_returnvalue(self): |
176 | - precache_rv = self.precache_result.config(offset=3, limit=2) |
177 | + prejoin_rv = self.prejoin_result.config(offset=3, limit=2) |
178 | standard_rv = self.standard_result.config(offset=3, limit=2) |
179 | - self.verify(precache_rv, standard_rv) |
180 | + self.verify(prejoin_rv, standard_rv) |
181 | |
182 | def test_order_by(self): |
183 | self.verify( |
184 | - self.precache_result.order_by(Product.id), |
185 | + self.prejoin_result.order_by(Product.id), |
186 | self.standard_result.order_by(Product.id)) |
187 | |
188 | def test_slice(self): |
189 | self.verify( |
190 | - self.precache_result[4:6], |
191 | + self.prejoin_result[4:6], |
192 | self.standard_result[4:6]) |
193 | |
194 | def test_any(self): |
195 | - self.assertIn(self.precache_result.any(), self.standard_result) |
196 | + self.assertIn(self.prejoin_result.any(), self.standard_result) |
197 | |
198 | def test_first(self): |
199 | self.assertEqual( |
200 | - self.standard_result.first(), self.precache_result.first()) |
201 | + self.standard_result.first(), self.prejoin_result.first()) |
202 | |
203 | def test_one(self): |
204 | standard_result = self.store.find(Product, Product.name == 'firefox') |
205 | - precache_result = precache(self.store.find( |
206 | + prejoin_result = prejoin(self.store.find( |
207 | (Product, Person), |
208 | Person.id == Product._ownerID, |
209 | Product.name == 'firefox')) |
210 | - self.assertEqual(standard_result.one(), precache_result.one()) |
211 | + self.assertEqual(standard_result.one(), prejoin_result.one()) |
212 | + |
213 | + def test_one_empty(self): |
214 | + # For an empty result (None), one returns None, too. |
215 | + name = "none-existent-name" |
216 | + prejoin_result = prejoin(self.store.find( |
217 | + (Product, Person), |
218 | + Person.id == Product._ownerID, |
219 | + Product.name == name)) |
220 | + self.assertIs(None, prejoin_result.one()) |
221 | |
222 | def test_cache_populated(self): |
223 | # Load a row. |
224 | - product = self.precache_result.first() |
225 | + product = self.prejoin_result.first() |
226 | |
227 | # Destroy our data, without telling Storm. This way we can |
228 | # tell if we are accessing an object from the cache, or if it |
229 | |
230 | === modified file 'lib/lp/translations/model/potemplate.py' |
231 | --- lib/lp/translations/model/potemplate.py 2009-10-27 10:40:46 +0000 |
232 | +++ lib/lp/translations/model/potemplate.py 2009-10-28 16:28:12 +0000 |
233 | @@ -24,7 +24,7 @@ |
234 | from sqlobject import ( |
235 | BoolCol, ForeignKey, IntCol, SQLMultipleJoin, SQLObjectNotFound, |
236 | StringCol) |
237 | -from storm.expr import Alias, And, Join, LeftJoin, Or, SQL |
238 | +from storm.expr import Alias, And, Desc, Join, LeftJoin, Or, SQL |
239 | from storm.info import ClassAlias |
240 | from storm.store import Store |
241 | from zope.component import getAdapter, getUtility |
242 | @@ -40,8 +40,10 @@ |
243 | from canonical.launchpad import helpers |
244 | from canonical.launchpad.interfaces.lpstorm import IStore |
245 | from lp.translations.utilities.rosettastats import RosettaStats |
246 | +from lp.services.database.prejoin import prejoin |
247 | from lp.services.worlddata.model.language import Language |
248 | from lp.registry.interfaces.person import validate_public_person |
249 | +from lp.registry.model.sourcepackagename import SourcePackageName |
250 | from lp.translations.model.pofile import POFile, DummyPOFile |
251 | from lp.translations.model.pomsgid import POMsgID |
252 | from lp.translations.model.potmsgset import POTMsgSet |
253 | @@ -1002,8 +1004,8 @@ |
254 | self.distroseries = distroseries |
255 | self.productseries = productseries |
256 | self.iscurrent = iscurrent |
257 | - self.clausetables = [] |
258 | - self.orderby = ['id'] |
259 | + self.orderby = [POTemplate.id] |
260 | + self.clauses = [] |
261 | |
262 | assert productseries is None or distroseries is None, ( |
263 | 'A product series must not be used with a distro series.') |
264 | @@ -1011,46 +1013,63 @@ |
265 | assert productseries is not None or distroseries is not None, ( |
266 | 'Either productseries or distroseries must be not None.') |
267 | |
268 | + # Construct the base clauses. |
269 | if productseries is not None: |
270 | - self.query = ('POTemplate.productseries = %s' % |
271 | - sqlvalues(productseries.id)) |
272 | - elif distroseries is not None and from_sourcepackagename is not None: |
273 | - self.query = ('POTemplate.from_sourcepackagename = %s AND' |
274 | - ' POTemplate.distroseries = %s ' % |
275 | - sqlvalues(from_sourcepackagename.id, |
276 | - distroseries.id)) |
277 | - self.sourcepackagename = from_sourcepackagename |
278 | - elif distroseries is not None and sourcepackagename is not None: |
279 | - self.query = ('POTemplate.sourcepackagename = %s AND' |
280 | - ' POTemplate.distroseries = %s ' % |
281 | - sqlvalues(sourcepackagename.id, distroseries.id)) |
282 | + self.clauses.append( |
283 | + POTemplate.productseriesID == productseries.id) |
284 | else: |
285 | - self.query = ( |
286 | - 'POTemplate.distroseries = DistroSeries.id AND' |
287 | - ' DistroSeries.id = %s' % sqlvalues(distroseries.id)) |
288 | - self.orderby.append('DistroSeries.name') |
289 | - self.clausetables.append('DistroSeries') |
290 | - |
291 | + self.clauses.append( |
292 | + POTemplate.distroseriesID == distroseries.id) |
293 | + if from_sourcepackagename is not None: |
294 | + self.clauses.append( |
295 | + POTemplate.from_sourcepackagenameID == |
296 | + from_sourcepackagename.id) |
297 | + self.sourcepackagename = from_sourcepackagename |
298 | + elif sourcepackagename is not None: |
299 | + self.clauses.append( |
300 | + POTemplate.sourcepackagename == sourcepackagename.id) |
301 | + else: |
302 | + # Select all POTemplates in a Distroseries. |
303 | + pass |
304 | # Add the filter for the iscurrent flag if requested. |
305 | if iscurrent is not None: |
306 | - self.query += " AND POTemplate.iscurrent=%s" % ( |
307 | - sqlvalues(iscurrent)) |
308 | - |
309 | - # Finally, we sort the query by its path in all cases. |
310 | - self.orderby.append('POTemplate.path') |
311 | + self.clauses.append( |
312 | + POTemplate.iscurrent == iscurrent) |
313 | + |
314 | + def _build_query(self, additional_clause=None, |
315 | + ordered=True, do_prejoin=True): |
316 | + """Construct the storm query.""" |
317 | + if additional_clause is None: |
318 | + condition = And(self.clauses) |
319 | + else: |
320 | + condition = And(self.clauses + [additional_clause]) |
321 | + |
322 | + if self.productseries is not None: |
323 | + store = Store.of(self.productseries) |
324 | + query = store.find(POTemplate, condition) |
325 | + else: |
326 | + store = Store.of(self.distroseries) |
327 | + if do_prejoin: |
328 | + query = prejoin(store.find( |
329 | + (POTemplate, SourcePackageName), |
330 | + (POTemplate.sourcepackagenameID == |
331 | + SourcePackageName.id), condition)) |
332 | + else: |
333 | + query = store.find(POTemplate, condition) |
334 | + |
335 | + if ordered: |
336 | + return query.order_by(self.orderby) |
337 | + return query |
338 | |
339 | def __iter__(self): |
340 | """See `IPOTemplateSubset`.""" |
341 | - res = POTemplate.select(self.query, clauseTables=self.clausetables, |
342 | - orderBy=self.orderby) |
343 | - |
344 | - for potemplate in res: |
345 | + for potemplate in self._build_query(): |
346 | yield potemplate |
347 | |
348 | def __len__(self): |
349 | """See `IPOTemplateSubset`.""" |
350 | - res = POTemplate.select(self.query, clauseTables=self.clausetables) |
351 | - return res.count() |
352 | + result = self._build_query(do_prejoin=False, ordered=False) |
353 | + return result.count() |
354 | |
355 | def __getitem__(self, name): |
356 | """See `IPOTemplateSubset`.""" |
357 | @@ -1109,24 +1128,16 @@ |
358 | |
359 | def getPOTemplateByName(self, name): |
360 | """See `IPOTemplateSubset`.""" |
361 | - queries = [self.query] |
362 | - clausetables = list(self.clausetables) |
363 | - queries.append('POTemplate.name = %s' % sqlvalues(name)) |
364 | - |
365 | - return POTemplate.selectOne(' AND '.join(queries), |
366 | - clauseTables=clausetables) |
367 | + result = self._build_query(POTemplate.name == name, ordered=False) |
368 | + return result.one() |
369 | |
370 | def getPOTemplateByTranslationDomain(self, translation_domain): |
371 | """See `IPOTemplateSubset`.""" |
372 | - queries = [self.query] |
373 | - clausetables = list(self.clausetables) |
374 | - |
375 | - queries.append('POTemplate.translation_domain = %s' % sqlvalues( |
376 | - translation_domain)) |
377 | + query_result = self._build_query( |
378 | + POTemplate.translation_domain == translation_domain) |
379 | |
380 | # Fetch up to 2 templates, to check for duplicates. |
381 | - matches = POTemplate.select( |
382 | - ' AND '.join(queries), clauseTables=clausetables, limit=2) |
383 | + matches = query_result.config(limit=2) |
384 | |
385 | result = [match for match in matches] |
386 | if len(result) == 0: |
387 | @@ -1144,23 +1155,14 @@ |
388 | |
389 | def getPOTemplateByPath(self, path): |
390 | """See `IPOTemplateSubset`.""" |
391 | - query = '%s AND POTemplate.path = %s' % (self.query, quote(path)) |
392 | - |
393 | - return POTemplate.selectOne(query, clauseTables=self.clausetables) |
394 | + result = self._build_query( |
395 | + POTemplate.path == path, ordered=False) |
396 | + return result.one() |
397 | |
398 | def getAllOrderByDateLastUpdated(self): |
399 | """See `IPOTemplateSet`.""" |
400 | - query = [] |
401 | - if self.productseries is not None: |
402 | - query.append('productseries = %s' % sqlvalues(self.productseries)) |
403 | - if self.distroseries is not None: |
404 | - query.append('distroseries = %s' % sqlvalues(self.distroseries)) |
405 | - if self.sourcepackagename is not None: |
406 | - query.append('sourcepackagename = %s' % sqlvalues( |
407 | - self.sourcepackagename)) |
408 | - |
409 | - return POTemplate.select( |
410 | - ' AND '.join(query), orderBy=['-date_last_updated']) |
411 | + result = self._build_query(ordered=False) |
412 | + return result.order_by(Desc(POTemplate.date_last_updated)) |
413 | |
414 | def getClosestPOTemplate(self, path): |
415 | """See `IPOTemplateSubset`.""" |
416 | @@ -1190,10 +1192,11 @@ |
417 | |
418 | def findUniquePathlessMatch(self, filename): |
419 | """See `IPOTemplateSubset`.""" |
420 | - query = self.query + ( |
421 | - " AND (POTemplate.path = %s OR POTemplate.path LIKE '%%/' || %s)" |
422 | - % (quote(filename), quote_like(filename))) |
423 | - candidates = list(POTemplate.select(query, limit=2)) |
424 | + result = self._build_query( |
425 | + ("(POTemplate.path = %s OR POTemplate.path LIKE '%%%%/' || %s)" |
426 | + % (quote(filename), quote_like(filename))), |
427 | + ordered=False) |
428 | + candidates = list(result.config(limit=2)) |
429 | |
430 | if len(candidates) == 1: |
431 | # Found exactly one match. |
= Bug 461818 =
The +templates page for distribution series was timing out with lots of storm objects being created by single-row SourcePackageName queries. This happened because the list of source packages for an Ubuntu distribution is quite long, obviously, and displays a SourcePackageNa me.name per line.
== Proposed solution ==
Prejoin SourcePackageName when querying the POTemplates for the list. That will prevent storm from fetching each SourcePackageName in its own query when it is referenced in the page template. The query happens in POTemplageSubSet.
== Implementation details ==
While the solution is quite straight forward, the implementation became a bit more complex because I decided to convert POTemplageSubSet to use storm and to do some clean-ups.
I made use of the "precache" function for storm which I renamed to "prejoin" as Stuart had suggested. You will find the result of this in the first 4 files of the patch. It also fixes a bug in prejoin that made it fail when "one()" was used on None which happens when the result set is empty. Really, I think storm should be returning an EmptyResultSet, but that is not the subject here.
The next (5th) file is model/potemplate.py which has the meat of this branch. Note that I moved the actual construction of the storm query into its own method so that only the base clauses are collected in the constructor. Note also, how I got rid of the useless sorting by path. The rest of the file translates quite nicely into storm.
One more point to note in this file is getAllOrderByDa teLastUpdated which used to reconstruct its own clauses with the difference that it did not take from_sourcepackage into account. I replaced that with the standard query for the SubSet (which does prefer from_sourcepack age). This did not break any tests but I am not all sure about the consequences.
== Test ==
I ran all of lp.translations on it because POTemplateSubSet is so widely used.
bin/test -vvt lp.translations -t prejoin
== Demo/QA ==
There should be no more timeouts on this page: /translations. staging. launchpad. net/ubuntu/ karmic/ +templates
https:/
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: registry/ model/product. py services/ database/ configure. zcml services/ database/ prejoin. py services/ database/ tests/test_ prejoin. py translations/ model/potemplat e.py
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
== Pylint notices ==
lib/lp/ registry/ model/product. py
29: [F0401] Unable to import 'lazr.delegates' (No module named delegates)
lib/lp/ services/ database/ prejoin. py
11: [F0401] Unable to import 'lazr.delegates' (No module named delegates)